diff --git a/CHANGELOG.md b/CHANGELOG.md index d4719ec..003a059 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Change Log +## [2.2.0] 2026-05-05 + +* [Pull #332](https://github.com/uber/uber-ios-sdk/pull/332) Allow client to choose the request environment + +## [2.1.0] 2025-12-04 + +* [Pull #330](https://github.com/uber/uber-ios-sdk/pull/330) Add async/await support for authentication methods + ## [0.14.0] 2023-04-18 * [Pull #278](https://github.com/uber/rides-ios-sdk/pull/278) Implementing PAR diff --git a/Package.swift b/Package.swift index 5ec918f..af54785 100644 --- a/Package.swift +++ b/Package.swift @@ -7,7 +7,7 @@ let package = Package( name: "rides-ios-sdk", defaultLocalization: "en", platforms: [ - .iOS(.v14) + .iOS(.v15) ], products: [ .library( diff --git a/Sources/UberAuth/AuthProviding.swift b/Sources/UberAuth/AuthProviding.swift index eb6b99b..20c46cb 100644 --- a/Sources/UberAuth/AuthProviding.swift +++ b/Sources/UberAuth/AuthProviding.swift @@ -32,6 +32,10 @@ public protocol AuthProviding { func execute(authDestination: AuthDestination, prefill: Prefill?, completion: @escaping (Result) -> ()) + + /// - Throws: `UberAuthError` + func execute(authDestination: AuthDestination, + prefill: Prefill?) async throws -> Client func logout() -> Bool @@ -45,12 +49,14 @@ extension AuthProviding where Self == AuthorizationCodeAuthProvider { public static func authorizationCode(presentationAnchor: ASPresentationAnchor = .init(), scopes: [String] = AuthorizationCodeAuthProvider.defaultScopes, shouldExchangeAuthCode: Bool = true, - prompt: Prompt? = nil) -> Self { + prompt: Prompt? = nil, + environment: UberEnvironment = .production) -> Self { AuthorizationCodeAuthProvider( presentationAnchor: presentationAnchor, scopes: scopes, shouldExchangeAuthCode: shouldExchangeAuthCode, - prompt: prompt + prompt: prompt, + environment: environment ) } } diff --git a/Sources/UberAuth/Authorize/AuthorizationCodeAuthProvider.swift b/Sources/UberAuth/Authorize/AuthorizationCodeAuthProvider.swift index a3fcc56..4724b65 100644 --- a/Sources/UberAuth/Authorize/AuthorizationCodeAuthProvider.swift +++ b/Sources/UberAuth/Authorize/AuthorizationCodeAuthProvider.swift @@ -67,15 +67,18 @@ public final class AuthorizationCodeAuthProvider: AuthProviding { private let tokenManager: TokenManaging private let scopes: [String] - + private let prompt: Prompt? - + + private let baseUrl: String + // MARK: Initializers - + public init(presentationAnchor: ASPresentationAnchor = .init(), scopes: [String] = AuthorizationCodeAuthProvider.defaultScopes, shouldExchangeAuthCode: Bool = false, - prompt: Prompt? = nil) { + prompt: Prompt? = nil, + environment: UberEnvironment = .production) { self.configurationProvider = ConfigurationProvider() self.applicationLauncher = UIApplication.shared self.authenticationSessionBuilder = nil @@ -84,12 +87,13 @@ public final class AuthorizationCodeAuthProvider: AuthProviding { self.redirectURI = configurationProvider.redirectURI self.responseParser = AuthorizationCodeResponseParser() self.shouldExchangeAuthCode = shouldExchangeAuthCode - self.networkProvider = NetworkProvider(baseUrl: Constants.baseUrl) - self.tokenManager = TokenManager() + self.baseUrl = environment.baseUrl + "/v2" + self.networkProvider = NetworkProvider(baseUrl: self.baseUrl) + self.tokenManager = TokenManager(environment: environment) self.scopes = scopes self.prompt = prompt } - + init(presentationAnchor: ASPresentationAnchor = .init(), authenticationSessionBuilder: AuthenticationSessionBuilder? = nil, scopes: [String] = AuthorizationCodeAuthProvider.defaultScopes, @@ -98,9 +102,10 @@ public final class AuthorizationCodeAuthProvider: AuthProviding { configurationProvider: ConfigurationProviding = ConfigurationProvider(), applicationLauncher: ApplicationLaunching = UIApplication.shared, responseParser: AuthorizationCodeResponseParsing = AuthorizationCodeResponseParser(), - networkProvider: NetworkProviding = NetworkProvider(baseUrl: Constants.baseUrl), - tokenManager: TokenManaging = TokenManager()) { - + networkProvider: NetworkProviding = NetworkProvider(baseUrl: UberEnvironment.production.baseUrl + "/v2"), + tokenManager: TokenManaging = TokenManager(), + environment: UberEnvironment = .production) { + self.applicationLauncher = applicationLauncher self.authenticationSessionBuilder = authenticationSessionBuilder self.clientID = configurationProvider.clientID @@ -109,6 +114,7 @@ public final class AuthorizationCodeAuthProvider: AuthProviding { self.redirectURI = configurationProvider.redirectURI self.responseParser = responseParser self.shouldExchangeAuthCode = shouldExchangeAuthCode + self.baseUrl = environment.baseUrl + "/v2" self.networkProvider = networkProvider self.tokenManager = tokenManager self.scopes = scopes @@ -116,7 +122,7 @@ public final class AuthorizationCodeAuthProvider: AuthProviding { } // MARK: AuthProviding - + public func execute(authDestination: AuthDestination, prefill: Prefill? = nil, completion: @escaping Completion) { @@ -156,6 +162,21 @@ public final class AuthorizationCodeAuthProvider: AuthProviding { self.completion = authCompletion } + + /// - Throws: `UberAuthError` + public func execute(authDestination: AuthDestination, + prefill: Prefill? = nil) async throws -> Client { + + let requestURI = await executePar(prefill: prefill) + + let client = try await executeLogin(authDestination: authDestination, requestURI: requestURI) + + if shouldExchangeAuthCode, let code = client.authorizationCode { + return try await exchange(code: code) + } + + return client + } public func logout() -> Bool { tokenManager.deleteToken(identifier: TokenManager.defaultAccessTokenIdentifier) @@ -177,7 +198,7 @@ public final class AuthorizationCodeAuthProvider: AuthProviding { } // MARK: - Private - + private func executeLogin(authDestination: AuthDestination, requestURI: String?, completion: @escaping Completion) { @@ -196,6 +217,17 @@ public final class AuthorizationCodeAuthProvider: AuthProviding { } } + func executeLogin(authDestination: AuthDestination, + requestURI: String?) async throws -> Client { + switch authDestination { + case .inApp: + try await executeInAppLogin(requestURI: requestURI) + case.native(let appPriority): + try await executeNativeLogin(appPriority: appPriority, requestURI: requestURI) + } + + } + /// Performs login using an embedded browser within the third party client. /// - Parameters: /// - completion: A closure to handle the login result @@ -218,11 +250,11 @@ public final class AuthorizationCodeAuthProvider: AuthProviding { scopes: scopes ) - guard let url = request.url(baseUrl: Constants.baseUrl) else { + guard let url = request.url(baseUrl: baseUrl) else { completion(.failure(.invalidRequest("Invalid base URL"))) return } - + guard let callbackURL = URL(string: redirectURI), let callbackURLScheme = callbackURL.scheme else { completion(.failure(.invalidRequest("Invalid redirect URI"))) @@ -243,6 +275,52 @@ public final class AuthorizationCodeAuthProvider: AuthProviding { currentSession?.start() } + + private func executeInAppLogin(requestURI: String?) async throws -> Client { + // Only execute one authentication session at a time + guard currentSession == nil else { throw UberAuthError.existingAuthSession } + + let request = AuthorizeRequest( + app: nil, + clientID: clientID, + codeChallenge: shouldExchangeAuthCode ? pkce.codeChallenge : nil, + prompt: prompt, + redirectURI: redirectURI, + requestURI: requestURI, + scopes: scopes + ) + + guard let url = request.url(baseUrl: baseUrl) else { + throw UberAuthError.invalidRequest("Invalid base URL") + } + + guard let callbackURL = URL(string: redirectURI), + let callbackURLScheme = callbackURL.scheme else { + throw UberAuthError.invalidRequest("Invalid redirect URI") + } + + return try await withCheckedThrowingContinuation { continuation in + if let sessionBuilder = authenticationSessionBuilder { + currentSession = sessionBuilder( + presentationAnchor, + callbackURLScheme, + url, + { [weak self] result in + continuation.resume(with: result) + self?.currentSession = nil + }) + } else { + currentSession = AuthenticationSession(anchor: presentationAnchor, callbackURLScheme: callbackURLScheme, url: url) { [weak self] result in + continuation.resume(with: result) + self?.currentSession = nil + + } + } + + currentSession?.start() + + } + } /// Performs login using one of the native Uber applications if available. /// @@ -295,6 +373,23 @@ public final class AuthorizationCodeAuthProvider: AuthProviding { ) } + private func executeNativeLogin(appPriority: [UberApp], requestURI: String?) async throws -> Client { + for app in appPriority { + let launched = await launch(context: (app, requestURI)) + + if launched { + return try await withCheckedThrowingContinuation { continuation in + self.completion = { result in + continuation.resume(with: result) + self.completion = nil + } + } + } + } + + return try await executeInAppLogin(requestURI: requestURI) + } + /// Attempts to launch a native app with an SSO universal link. /// Calls a closure with a boolean indicating if the application was successfully opened. /// @@ -326,11 +421,11 @@ public final class AuthorizationCodeAuthProvider: AuthProviding { scopes: scopes ) - guard let url = request.url(baseUrl: Constants.baseUrl) else { + guard let url = request.url(baseUrl: baseUrl) else { completion?(false) return } - + DispatchQueue.main.async { self.applicationLauncher.launch( url, @@ -340,6 +435,31 @@ public final class AuthorizationCodeAuthProvider: AuthProviding { ) } } + + func launch(context: (app: UberApp, requestURI: String?)) async -> Bool { + let (app, requestURI) = context + guard configurationProvider.isInstalled(app: app, defaultIfUnregistered: true) else { return false } + + // .login not supported for native auth + var prompt = prompt + prompt?.remove(.login) + + let request = AuthorizeRequest( + app: app, + clientID: clientID, + codeChallenge: shouldExchangeAuthCode ? pkce.codeChallenge : nil, + prompt: prompt, + redirectURI: redirectURI, + requestURI: requestURI, + scopes: scopes + ) + + guard let url = request.url(baseUrl: baseUrl) else { return false } + + return await applicationLauncher.launch(url) + } + + private func executePar(prefill: Prefill?, completion: @escaping (_ requestURI: String?) -> Void) { @@ -366,6 +486,16 @@ public final class AuthorizationCodeAuthProvider: AuthProviding { ) } + func executePar(prefill: Prefill?) async -> String? { + guard let prefill else { return nil } + + let request = ParRequest(clientID: clientID, prefill: prefill.dictValue) + + let result = try? await networkProvider.execute(request: request) + + return result?.requestURI + } + // MARK: Token Exchange /// Makes a request to the /token endpoing to exchange the authorization code @@ -386,7 +516,7 @@ public final class AuthorizationCodeAuthProvider: AuthProviding { case .success(let response): let client = Client(tokenResponse: response) if let accessToken = client.accessToken { - self?.tokenManager.saveToken( + _ = self?.tokenManager.saveToken( accessToken, identifier: TokenManager.defaultAccessTokenIdentifier ) @@ -399,13 +529,28 @@ public final class AuthorizationCodeAuthProvider: AuthProviding { ) } - // MARK: Constants - - private enum Constants { - static let clientIDKey = "ClientID" - static let redirectURI = "RedirectURI" - static let baseUrl = "https://auth.uber.com/v2" + func exchange(code: String) async throws -> Client { + let request = TokenRequest( + clientID: clientID, + authorizationCode: code, + redirectURI: redirectURI, + codeVerifier: pkce.codeVerifier + ) + + let response = try await networkProvider.execute(request: request) + + let client = Client(tokenResponse: response) + if let accessToken = client.accessToken { + _ = tokenManager + .saveToken( + accessToken, + identifier: TokenManager.defaultAccessTokenIdentifier + ) + } + + return client } + } diff --git a/Sources/UberAuth/Networking/NetworkProvider.swift b/Sources/UberAuth/Networking/NetworkProvider.swift index 69c5e89..768635f 100644 --- a/Sources/UberAuth/Networking/NetworkProvider.swift +++ b/Sources/UberAuth/Networking/NetworkProvider.swift @@ -28,6 +28,7 @@ import Foundation /// @mockable protocol NetworkProviding { func execute(request: R, completion: @escaping (Result) -> ()) + func execute(request: R) async throws -> R.Response } final class NetworkProvider: NetworkProviding { @@ -40,7 +41,6 @@ final class NetworkProvider: NetworkProviding { self.baseUrl = baseUrl self.session = URLSession(configuration: .default) } - func execute(request: R, completion: @escaping (Result) -> ()) { guard let urlRequest = request.urlRequest(baseUrl: baseUrl) else { completion(.failure(UberAuthError.invalidRequest(""))) @@ -78,4 +78,33 @@ final class NetworkProvider: NetworkProviding { dataTask.resume() } + + func execute(request: R) async throws -> R.Response { + guard let urlRequest = request.urlRequest(baseUrl: baseUrl) else { + throw UberAuthError.invalidRequest("") + } + + let (data, response): (Data, URLResponse) + do { + (data, response) = try await session.data(for: urlRequest) + } catch { + throw UberAuthError.other(error) + } + + guard let httpResponse = response as? HTTPURLResponse else { + throw UberAuthError.oAuth(.unsupportedResponseType) + } + + if let error = UberAuthError(httpResponse) { + throw error + } + + do { + let decodedResponse = try decoder.decode(R.Response.self, from: data) + return decodedResponse + } catch { + throw UberAuthError.serviceError + } + } } + diff --git a/Sources/UberAuth/Token/TokenManager.swift b/Sources/UberAuth/Token/TokenManager.swift index 81772e7..b92e52f 100644 --- a/Sources/UberAuth/Token/TokenManager.swift +++ b/Sources/UberAuth/Token/TokenManager.swift @@ -24,6 +24,7 @@ import Foundation +import UberCore /// @mockable public protocol TokenManaging { @@ -66,15 +67,19 @@ public extension TokenManaging { } public final class TokenManager: TokenManaging { - + public static let defaultAccessTokenIdentifier: String = "UberAccessTokenKey" - + public static let defaultKeychainAccessGroup: String = "" - + private let keychainUtility: KeychainUtilityProtocol - - public init(keychainUtility: KeychainUtilityProtocol = KeychainUtility()) { + + private let regionHost: String + + public init(keychainUtility: KeychainUtilityProtocol = KeychainUtility(), + environment: UberEnvironment = .production) { self.keychainUtility = keychainUtility + self.regionHost = environment.baseUrl } // MARK: Save @@ -128,9 +133,9 @@ public final class TokenManager: TokenManaging { // MARK: Private Interface - /// Removes all cookies in the shared cookie store corresponding with the auth.uber.com domain + /// Removes all cookies in the shared cookie store corresponding with the auth domain private func deleteCookies() { - guard let loginUrl = URL(string: Constants.regionHost) else { + guard let loginUrl = URL(string: regionHost) else { return } @@ -142,10 +147,4 @@ public final class TokenManager: TokenManaging { } } } - - // MARK: Constants - - private enum Constants { - static let regionHost = "https://auth.uber.com" - } } diff --git a/Sources/UberAuth/UberAuth.swift b/Sources/UberAuth/UberAuth.swift index ee7af4b..7cd7522 100644 --- a/Sources/UberAuth/UberAuth.swift +++ b/Sources/UberAuth/UberAuth.swift @@ -29,20 +29,44 @@ public typealias AuthCompletion = (Result) -> () /// @mockable public protocol UberAuthInterface { - + /// Executes a single login session using the provided context /// /// - Parameters: /// - context: An `AuthContext` instance providing all information needed to execute authentication /// - completion: A closure to be called upon completion static func login(context: AuthContext, completion: @escaping AuthCompletion) - - + /// Clears any saved auth information from the keychain /// If `currentAuthContext` exists, logs out using the stored auth context /// Otherwise, attempts to delete the saved auth token directly using the internal TokenManager static func logout() - + + /// Attempts to extract auth information from the provided URL. + /// This method should be called from the implemeting application's openURL function. + /// + /// - Parameter url: The URL that was passed into the implementing app + /// - Returns: A boolean indicating if the URL was handled or not + static func handle(_ url: URL) -> Bool +} + +/// Protocol for async/await based authentication +/// @mockable +public protocol UberAuthAsyncInterface { + + /// Executes a single login session using the provided context + /// + /// - Parameters: + /// - context: An `AuthContext` instance providing all information needed to execute authentication + /// - Returns: The authenticated client + /// - Throws: `UberAuthError` + static func login(context: AuthContext) async throws -> Client + + /// Clears any saved auth information from the keychain + /// If `currentAuthContext` exists, logs out using the stored auth context + /// Otherwise, attempts to delete the saved auth token directly using the internal TokenManager + static func logout() + /// Attempts to extract auth information from the provided URL. /// This method should be called from the implemeting application's openURL function. /// @@ -57,8 +81,10 @@ public protocol UberAuthInterface { /// /// @mockable protocol AuthManaging { - + func login(context: AuthContext, completion: @escaping AuthCompletion) + + func login(context: AuthContext) async throws -> Client func logout() @@ -68,7 +94,7 @@ protocol AuthManaging { } /// Public interface for the uber-auth-ios library -public final class UberAuth: UberAuthInterface, AuthManaging { +public final class UberAuth: UberAuthInterface, UberAuthAsyncInterface, AuthManaging { // MARK: Public @@ -85,6 +111,10 @@ public final class UberAuth: UberAuthInterface, AuthManaging { ) } + public static func login(context: AuthContext = .init()) async throws -> Client { + try await auth.login(context: context) + } + /// Clears any saved auth information from the keychain /// If `currentAuthContext` exists, logs out using the stored auth context /// Otherwise, attempts to delete the saved auth token directly using the internal TokenManager @@ -128,6 +158,15 @@ public final class UberAuth: UberAuthInterface, AuthManaging { currentContext = context } + func login(context: AuthContext = .init()) async throws -> Client { + let client = try await context.authProvider.execute( + authDestination: context.authDestination, + prefill: context.prefill + ) + currentContext = context + return client + } + func logout() { guard let currentContext else { tokenManager.deleteToken(identifier: TokenManager.defaultAccessTokenIdentifier) diff --git a/Sources/UberCore/ApplicationLauncher.swift b/Sources/UberCore/ApplicationLauncher.swift index b9f5ceb..a4cf729 100644 --- a/Sources/UberCore/ApplicationLauncher.swift +++ b/Sources/UberCore/ApplicationLauncher.swift @@ -28,15 +28,21 @@ import UIKit /// @mockable public protocol ApplicationLaunching { - + func launch(_ url: URL, completion: ((Bool) -> ())?) + + func launch(_ url: URL) async -> Bool } extension UIApplication: ApplicationLaunching { - + public func launch(_ url: URL, completion: ((Bool) -> ())?) { open(url, options: [:]) { completion?($0) } } + + public func launch(_ url: URL) async -> Bool { + await open(url, options: [:]) + } } diff --git a/Sources/UberCore/Info.plist b/Sources/UberCore/Info.plist index 6d05565..548b0a3 100644 --- a/Sources/UberCore/Info.plist +++ b/Sources/UberCore/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 0.13.0 + 2.2.0 CFBundleVersion $(CURRENT_PROJECT_VERSION) NSPrincipalClass diff --git a/Sources/UberCore/UberEnvironment.swift b/Sources/UberCore/UberEnvironment.swift new file mode 100644 index 0000000..aea33a3 --- /dev/null +++ b/Sources/UberCore/UberEnvironment.swift @@ -0,0 +1,37 @@ +// +// UberEnvironment.swift +// UberCore +// +// Copyright © 2024 Uber Technologies, Inc. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +public enum UberEnvironment { + case production + case sandbox + + public var baseUrl: String { + switch self { + case .production: return "https://auth.uber.com" + case .sandbox: return "https://sandbox-login.uber.com" + } + } +} diff --git a/Sources/UberRides/Info.plist b/Sources/UberRides/Info.plist index 4fb40c5..a60ea4a 100644 --- a/Sources/UberRides/Info.plist +++ b/Sources/UberRides/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 0.13.0 + 2.2.0 CFBundleSignature ???? CFBundleVersion diff --git a/examples/UberSDK/UberSDK/ContentView.swift b/examples/UberSDK/UberSDK/ContentView.swift index 3fe2082..803413e 100644 --- a/examples/UberSDK/UberSDK/ContentView.swift +++ b/examples/UberSDK/UberSDK/ContentView.swift @@ -48,25 +48,39 @@ final class Content { var selection: Item? var type: LoginType? = .authorizationCode var destination: LoginDestination? = .inApp + var environment: LoginEnvironment? = .production var isTokenExchangeEnabled: Bool = true var shouldForceLogin: Bool = false var shouldForceConsent: Bool = false var isPrefillExpanded: Bool = false var response: AuthReponse? var prefillBuilder = PrefillBuilder() + var loginTask: Task? var isLoggedIn: Bool { UberAuth.isLoggedIn } func login() { - + loginTask?.cancel() + loginTask = Task { + do { + try await login() + } catch { + response = AuthReponse(value: error.localizedDescription) + } + loginTask = nil + } + } + + private func login() async throws { var prompt: Prompt = [] if shouldForceLogin { prompt.insert(.login) } if shouldForceConsent { prompt.insert(.consent) } - + let authProvider: AuthProviding = .authorizationCode( shouldExchangeAuthCode: isTokenExchangeEnabled, - prompt: prompt + prompt: prompt, + environment: environment == .sandbox ? .sandbox : .production ) let authDestination: AuthDestination = { @@ -77,24 +91,15 @@ final class Content { } }() - UberAuth.login( + let client = try await UberAuth.login( context: .init( authDestination: authDestination, authProvider: authProvider, prefill: isPrefillExpanded ? prefillBuilder.prefill : nil - ), - completion: { result in - // Slight delay to allow for ASWebAuthenticationSession dismissal - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - switch result { - case .success(let client): - self.response = AuthReponse(value: "\(client)") - case .failure(let error): - self.response = AuthReponse(value: error.localizedDescription) - } - } - } + ) ) + + response = AuthReponse(value: "\(client)") } func logout() { @@ -106,9 +111,14 @@ final class Content { UberAuth.handle(url) } + deinit { + loginTask?.cancel() + } + enum Item: String, Hashable, Identifiable { case type = "Auth Type" case destination = "Destination" + case environment = "Environment" case tokenExchange = "Exchange Auth Code for Token" case forceLogin = "Always ask for Login" case forceConsent = "Always ask for Consent" @@ -159,6 +169,12 @@ struct ContentView: View { options: LoginDestination.allCases ) .presentationDetents([.height(200)]) + case .environment: + SelectionView( + selection: $content.environment, + options: LoginEnvironment.allCases + ) + .presentationDetents([.height(200)]) default: EmptyView() } @@ -194,6 +210,7 @@ struct ContentView: View { textRow(.type, value: content.type?.description) textRow(.destination, value: content.destination?.description) + textRow(.environment, value: content.environment?.description) toggleRow(.tokenExchange, value: $content.isTokenExchangeEnabled) toggleRow(.forceLogin, value: $content.shouldForceLogin) toggleRow(.forceConsent, value: $content.shouldForceConsent) diff --git a/examples/UberSDK/UberSDK/Info.plist b/examples/UberSDK/UberSDK/Info.plist index 0d0e4e4..c3dc43b 100644 --- a/examples/UberSDK/UberSDK/Info.plist +++ b/examples/UberSDK/UberSDK/Info.plist @@ -24,9 +24,9 @@ Uber ClientID - [Client ID] + u6fWe9U2aQv1hE-dPXhzPXmnmOD45n6N RedirectURI - com.uber.UberSDK://oauth/consumer + https://www.uber.com/ DisplayName [App Name] diff --git a/examples/UberSDK/UberSDK/SelectionOptions.swift b/examples/UberSDK/UberSDK/SelectionOptions.swift index f415100..58aa06e 100644 --- a/examples/UberSDK/UberSDK/SelectionOptions.swift +++ b/examples/UberSDK/UberSDK/SelectionOptions.swift @@ -35,8 +35,16 @@ enum LoginType: String, CaseIterable, SelectionOption { enum LoginDestination: String, CaseIterable, SelectionOption { case inApp = "In App" - case native = "Native" - + case native = "Native" + + var description: String { rawValue } + var id: String { rawValue } +} + +enum LoginEnvironment: String, CaseIterable, SelectionOption { + case production = "Production" + case sandbox = "Sandbox" + var description: String { rawValue } var id: String { rawValue } } diff --git a/examples/UberSDK/UberSDKTests/Mocks/Mocks.swift b/examples/UberSDK/UberSDKTests/Mocks/Mocks.swift index 678fbbc..9ed92c1 100644 --- a/examples/UberSDK/UberSDKTests/Mocks/Mocks.swift +++ b/examples/UberSDK/UberSDKTests/Mocks/Mocks.swift @@ -11,12 +11,12 @@ import UIKit @testable import UberCore -public class TokenManagingMock: TokenManaging { +public final class TokenManagingMock: TokenManaging { public init() { } public private(set) var saveTokenCallCount = 0 - public var saveTokenHandler: ((AccessToken, String, String?) -> (Bool))? + public var saveTokenHandler: ((AccessToken, String, String?) -> Bool)? public func saveToken(_ token: AccessToken, identifier: String, accessGroup: String?) -> Bool { saveTokenCallCount += 1 if let saveTokenHandler = saveTokenHandler { @@ -26,7 +26,7 @@ public class TokenManagingMock: TokenManaging { } public private(set) var getTokenCallCount = 0 - public var getTokenHandler: ((String, String?) -> (AccessToken?))? + public var getTokenHandler: ((String, String?) -> AccessToken?)? public func getToken(identifier: String, accessGroup: String?) -> AccessToken? { getTokenCallCount += 1 if let getTokenHandler = getTokenHandler { @@ -36,7 +36,7 @@ public class TokenManagingMock: TokenManaging { } public private(set) var deleteTokenCallCount = 0 - public var deleteTokenHandler: ((String, String?) -> (Bool))? + public var deleteTokenHandler: ((String, String?) -> Bool)? public func deleteToken(identifier: String, accessGroup: String?) -> Bool { deleteTokenCallCount += 1 if let deleteTokenHandler = deleteTokenHandler { @@ -46,27 +46,12 @@ public class TokenManagingMock: TokenManaging { } } -class NetworkProvidingMock: NetworkProviding { - init() { } - - - private(set) var executeCallCount = 0 - var executeHandler: ((Any, Any) -> ())? - func execute(request: R, completion: @escaping (Result) -> ()) { - executeCallCount += 1 - if let executeHandler = executeHandler { - executeHandler(request, completion) - } - - } -} - -public class KeychainUtilityProtocolMock: KeychainUtilityProtocol { +public final class KeychainUtilityProtocolMock: KeychainUtilityProtocol { public init() { } public private(set) var saveCallCount = 0 - public var saveHandler: ((Any, String, String?) -> (Bool))? + public var saveHandler: ((Any, String, String?) -> Bool)? public func save(_ value: V, for key: String, accessGroup: String?) -> Bool { saveCallCount += 1 if let saveHandler = saveHandler { @@ -76,7 +61,7 @@ public class KeychainUtilityProtocolMock: KeychainUtilityProtocol { } public private(set) var getCallCount = 0 - public var getHandler: ((String, String?) -> (Any?))? + public var getHandler: ((String, String?) -> Any?)? public func get(key: String, accessGroup: String?) -> V? { getCallCount += 1 if let getHandler = getHandler { @@ -86,7 +71,7 @@ public class KeychainUtilityProtocolMock: KeychainUtilityProtocol { } public private(set) var deleteCallCount = 0 - public var deleteHandler: ((String, String?) -> (Bool))? + public var deleteHandler: ((String, String?) -> Bool)? public func delete(key: String, accessGroup: String?) -> Bool { deleteCallCount += 1 if let deleteHandler = deleteHandler { @@ -96,12 +81,41 @@ public class KeychainUtilityProtocolMock: KeychainUtilityProtocol { } } -class AuthorizationCodeResponseParsingMock: AuthorizationCodeResponseParsing { +final class NetworkProvidingMock: NetworkProviding { + init() { } + + private(set) var executeCallCount = 0 + var executeHandler: ((Any, Any) -> ())? + func execute(request: R, completion: @escaping (Result) -> ()) { + executeCallCount += 1 + if let executeHandler = executeHandler { + executeHandler(request, completion) + } + + } + + var executeAsyncResult: Result? + func execute(request: R) async throws -> R.Response { + executeCallCount += 1 + + guard let result = executeAsyncResult else { throw UberAuthError.serviceError } + + switch result { + case .success(let response): + guard let typedResponse = response as? R.Response else { throw UberAuthError.serviceError } + return typedResponse + case .failure(let error): + throw error + } + } +} + +final class AuthorizationCodeResponseParsingMock: AuthorizationCodeResponseParsing { init() { } private(set) var isValidResponseCallCount = 0 - var isValidResponseHandler: ((URL, String) -> (Bool))? + var isValidResponseHandler: ((URL, String) -> Bool)? func isValidResponse(url: URL, matching redirectURI: String) -> Bool { isValidResponseCallCount += 1 if let isValidResponseHandler = isValidResponseHandler { @@ -111,7 +125,7 @@ class AuthorizationCodeResponseParsingMock: AuthorizationCodeResponseParsing { } private(set) var callAsFunctionCallCount = 0 - var callAsFunctionHandler: ((URL) -> (Result))? + var callAsFunctionHandler: ((URL) -> Result)? func callAsFunction(url: URL) -> Result { callAsFunctionCallCount += 1 if let callAsFunctionHandler = callAsFunctionHandler { @@ -121,22 +135,29 @@ class AuthorizationCodeResponseParsingMock: AuthorizationCodeResponseParsing { } } -public class ApplicationLaunchingMock: ApplicationLaunching { +public final class ApplicationLaunchingMock: ApplicationLaunching { public init() { } public private(set) var launchCallCount = 0 public var launchHandler: ((URL, ((Bool) -> ())?) -> ())? - public func launch(_ url: URL, completion: ((Bool) -> ())?) { + public func launch(_ url: URL, completion: ((Bool) -> ())?) { launchCallCount += 1 if let launchHandler = launchHandler { launchHandler(url, completion) } } + + public private(set) var launchUrlCallCount = 0 + public var launchResult: Bool = false + public func launch(_ url: URL) async -> Bool { + launchUrlCallCount += 1 + return launchResult + } } -public class ConfigurationProvidingMock: ConfigurationProviding { +public final class ConfigurationProvidingMock: ConfigurationProviding { public init() { } public init(clientID: String = "", redirectURI: String = "", sdkVersion: String = "", serverToken: String? = nil) { self.clientID = clientID @@ -146,27 +167,23 @@ public class ConfigurationProvidingMock: ConfigurationProviding { } - public private(set) var clientIDSetCallCount = 0 - public var clientID: String = "" { didSet { clientIDSetCallCount += 1 } } - public private(set) var redirectURISetCallCount = 0 - public var redirectURI: String = "" { didSet { redirectURISetCallCount += 1 } } + public var clientID: String = "" - public private(set) var sdkVersionSetCallCount = 0 - public var sdkVersion: String = "" { didSet { sdkVersionSetCallCount += 1 } } - public private(set) var serverTokenSetCallCount = 0 - public var serverToken: String? = nil { didSet { serverTokenSetCallCount += 1 } } + public var redirectURI: String = "" + + + public var sdkVersion: String = "" - public static private(set) var isSandboxSetCallCount = 0 - static private var _isSandbox: Bool = false { didSet { isSandboxSetCallCount += 1 } } - public static var isSandbox: Bool { - get { return _isSandbox } - set { _isSandbox = newValue } - } + + public var serverToken: String? = nil + + + public static var isSandbox: Bool = false public private(set) var isInstalledCallCount = 0 - public var isInstalledHandler: ((UberApp, Bool) -> (Bool))? + public var isInstalledHandler: ((UberApp, Bool) -> Bool)? public func isInstalled(app: UberApp, defaultIfUnregistered: Bool) -> Bool { isInstalledCallCount += 1 if let isInstalledHandler = isInstalledHandler { @@ -176,8 +193,8 @@ public class ConfigurationProvidingMock: ConfigurationProviding { } } -class AuthenticationSessioningMock: AuthenticationSessioning { - private var _anchor: ASPresentationAnchor! +final class AuthenticationSessioningMock: AuthenticationSessioning { + private var _anchor: ASPresentationAnchor! private var _callbackURLScheme: String! private var _completion: AuthCompletion! private var _url: URL! @@ -192,7 +209,7 @@ class AuthenticationSessioningMock: AuthenticationSessioning { private(set) var startCallCount = 0 var startHandler: (() -> ())? - func start() { + func start() { startCallCount += 1 if let startHandler = startHandler { startHandler() @@ -201,7 +218,7 @@ class AuthenticationSessioningMock: AuthenticationSessioning { } } -public class AuthProvidingMock: AuthProviding { +public final class AuthProvidingMock: AuthProviding { public init() { } public init(isLoggedIn: Bool = false) { self.isLoggedIn = isLoggedIn @@ -210,16 +227,30 @@ public class AuthProvidingMock: AuthProviding { public private(set) var executeCallCount = 0 public var executeHandler: ((AuthDestination, Prefill?, @escaping (Result) -> ()) -> ())? - public func execute(authDestination: AuthDestination, prefill: Prefill?, completion: @escaping (Result) -> ()) { + public func execute(authDestination: AuthDestination, prefill: Prefill?, completion: @escaping (Result) -> ()) { executeCallCount += 1 if let executeHandler = executeHandler { executeHandler(authDestination, prefill, completion) } } + + public var executeAsyncResult: Result? + public func execute(authDestination: AuthDestination, prefill: Prefill?) async throws -> Client { + executeCallCount += 1 + + guard let result = executeAsyncResult else { throw UberAuthError.serviceError } + + switch result { + case .success(let client): + return client + case .failure(let error): + throw error + } + } public private(set) var logoutCallCount = 0 - public var logoutHandler: (() -> (Bool))? + public var logoutHandler: (() -> Bool)? public func logout() -> Bool { logoutCallCount += 1 if let logoutHandler = logoutHandler { @@ -229,7 +260,7 @@ public class AuthProvidingMock: AuthProviding { } public private(set) var handleCallCount = 0 - public var handleHandler: ((URL) -> (Bool))? + public var handleHandler: ((URL) -> Bool)? public func handle(response url: URL) -> Bool { handleCallCount += 1 if let handleHandler = handleHandler { @@ -238,27 +269,39 @@ public class AuthProvidingMock: AuthProviding { return false } - public private(set) var isLoggedInSetCallCount = 0 - public var isLoggedIn: Bool = false { didSet { isLoggedInSetCallCount += 1 } } + public var isLoggedIn: Bool = false } -public class UberAuthInterfaceMock: UberAuthInterface { +public final class UberAuthInterfaceMock: UberAuthInterface { public init() { } - public static private(set) var loginCallCount = 0 public static var loginHandler: ((AuthContext, @escaping AuthCompletion) -> ())? - public static func login(context: AuthContext, completion: @escaping AuthCompletion) { + public static func login(context: AuthContext, completion: @escaping AuthCompletion) { loginCallCount += 1 if let loginHandler = loginHandler { loginHandler(context, completion) } } + + public static var loginAsyncResult: Result? + public static func login(context: AuthContext) async throws -> Client { + loginCallCount += 1 + + guard let result = loginAsyncResult else { throw UberAuthError.serviceError } + + switch result { + case .success(let client): + return client + case .failure(let error): + throw error + } + } public static private(set) var logoutCallCount = 0 public static var logoutHandler: (() -> ())? - public static func logout() { + public static func logout() { logoutCallCount += 1 if let logoutHandler = logoutHandler { logoutHandler() @@ -267,7 +310,7 @@ public class UberAuthInterfaceMock: UberAuthInterface { } public static private(set) var handleCallCount = 0 - public static var handleHandler: ((URL) -> (Bool))? + public static var handleHandler: ((URL) -> Bool)? public static func handle(_ url: URL) -> Bool { handleCallCount += 1 if let handleHandler = handleHandler { @@ -277,26 +320,39 @@ public class UberAuthInterfaceMock: UberAuthInterface { } } -class AuthManagingMock: AuthManaging { +final class AuthManagingMock: AuthManaging { init() { } init(isLoggedIn: Bool = false) { self.isLoggedIn = isLoggedIn } - private(set) var loginCallCount = 0 var loginHandler: ((AuthContext, @escaping AuthCompletion) -> ())? - func login(context: AuthContext, completion: @escaping AuthCompletion) { + func login(context: AuthContext, completion: @escaping AuthCompletion) { loginCallCount += 1 if let loginHandler = loginHandler { loginHandler(context, completion) } } + + var loginAsyncResult: Result? + func login(context: AuthContext) async throws -> Client { + loginCallCount += 1 + + guard let result = loginAsyncResult else { throw UberAuthError.serviceError } + + switch result { + case .success(let client): + return client + case .failure(let error): + throw error + } + } private(set) var logoutCallCount = 0 var logoutHandler: (() -> ())? - func logout() { + func logout() { logoutCallCount += 1 if let logoutHandler = logoutHandler { logoutHandler() @@ -305,7 +361,7 @@ class AuthManagingMock: AuthManaging { } private(set) var handleCallCount = 0 - var handleHandler: ((URL) -> (Bool))? + var handleHandler: ((URL) -> Bool)? func handle(_ url: URL) -> Bool { handleCallCount += 1 if let handleHandler = handleHandler { @@ -314,7 +370,5 @@ class AuthManagingMock: AuthManaging { return false } - private(set) var isLoggedInSetCallCount = 0 - var isLoggedIn: Bool = false { didSet { isLoggedInSetCallCount += 1 } } + var isLoggedIn: Bool = false } - diff --git a/examples/UberSDK/UberSDKTests/UberAuth/AuthManagerTests.swift b/examples/UberSDK/UberSDKTests/UberAuth/AuthManagerTests.swift index 3c084e8..d0e5231 100644 --- a/examples/UberSDK/UberSDKTests/UberAuth/AuthManagerTests.swift +++ b/examples/UberSDK/UberSDKTests/UberAuth/AuthManagerTests.swift @@ -305,3 +305,33 @@ final class UberAuthTests: XCTestCase { XCTAssertEqual(tokenManager.deleteTokenCallCount, 1) } } + +@MainActor +extension UberAuthTests { + + func test_login_async_success() async throws { + let authProvider = AuthProvidingMock() + authProvider.executeAsyncResult = .success(Client(authorizationCode: "code")) + + let context = AuthContext(authDestination: .inApp, authProvider: authProvider, prefill: nil) + + let client = try await uberAuth.login(context: context) + + XCTAssertNotNil(client.authorizationCode) + XCTAssertEqual(authProvider.executeCallCount, 1) + } + + func test_login_async_error() async { + let authProvider = AuthProvidingMock() + authProvider.executeAsyncResult = .failure(UberAuthError.serviceError) + + let context = AuthContext(authDestination: .inApp, authProvider: authProvider, prefill: nil) + + do { + _ = try await uberAuth.login(context: context) + XCTFail("Should have thrown error") + } catch { + XCTAssertNotNil(error as? UberAuthError) + } + } +} diff --git a/examples/UberSDK/UberSDKTests/UberAuth/AuthorizationCodeAuthProviderTests.swift b/examples/UberSDK/UberSDKTests/UberAuth/AuthorizationCodeAuthProviderTests.swift index ff81325..c611d75 100644 --- a/examples/UberSDK/UberSDKTests/UberAuth/AuthorizationCodeAuthProviderTests.swift +++ b/examples/UberSDK/UberSDKTests/UberAuth/AuthorizationCodeAuthProviderTests.swift @@ -712,3 +712,394 @@ final class AuthorizationCodeAuthProviderTests: XCTestCase { wait(for: [expectation], timeout: 0.1) } } + +@MainActor +extension AuthorizationCodeAuthProviderTests { + + func test_executePar_async_withNoPrefill_returnsNil() async { + let provider = AuthorizationCodeAuthProvider(configurationProvider: configurationProvider) + + let result = await provider.executePar(prefill: nil) + + XCTAssertNil(result) + } + + func test_executePar_async_withPrefill_success_returnsRequestURI() async { + let mockNetwork = NetworkProvidingMock() + let expectedRequestURI = "urn:test:request:uri:12345" + mockNetwork.executeAsyncResult = .success( + Par( + requestURI: expectedRequestURI, + expiresIn: Date(timeIntervalSinceNow: 3600) + ) + ) + + let provider = AuthorizationCodeAuthProvider( + configurationProvider: configurationProvider, + networkProvider: mockNetwork + ) + + let prefill = Prefill(email: "test@example.com") + + let result = await provider.executePar(prefill: prefill) + + XCTAssertEqual(result, expectedRequestURI) + XCTAssertEqual(mockNetwork.executeCallCount, 1) + } + + func test_executePar_async_withPrefill_failure_returnsNil() async { + let mockNetwork = NetworkProvidingMock() + mockNetwork.executeAsyncResult = .failure(UberAuthError.serviceError) + + let provider = AuthorizationCodeAuthProvider( + configurationProvider: configurationProvider, + networkProvider: mockNetwork + ) + + let prefill = Prefill(email: "test@example.com") + + let result = await provider.executePar(prefill: prefill) + + XCTAssertNil(result, "executePar should return nil on failure (PAR is optional)") + XCTAssertEqual(mockNetwork.executeCallCount, 1) + } + + func test_exchange_async_success_returnsClientAndSavesToken() async throws { + let mockNetwork = NetworkProvidingMock() + let mockTokenManager = TokenManagingMock() + + let expectedToken = AccessToken( + tokenString: "access_token_abc123", + refreshToken: "refresh_token_xyz789", + tokenType: "Bearer", + expiresIn: 3600, + scope: ["profile", "history"] + ) + + mockNetwork.executeAsyncResult = .success(expectedToken) + mockTokenManager.saveTokenHandler = { _, _, _ in true } + + let provider = AuthorizationCodeAuthProvider( + configurationProvider: configurationProvider, + networkProvider: mockNetwork, + tokenManager: mockTokenManager + ) + + let client = try await provider.exchange(code: "auth_code_test_123") + + XCTAssertEqual(client.accessToken, expectedToken) + XCTAssertEqual(mockTokenManager.saveTokenCallCount, 1) + XCTAssertEqual(mockNetwork.executeCallCount, 1) + } + + func test_exchange_async_error_throwsAndDoesNotSaveToken() async { + let mockNetwork = NetworkProvidingMock() + mockNetwork.executeAsyncResult = .failure(UberAuthError.serviceError) + + let mockTokenManager = TokenManagingMock() + + let provider = AuthorizationCodeAuthProvider( + configurationProvider: configurationProvider, + networkProvider: mockNetwork, + tokenManager: mockTokenManager + ) + + do { + _ = try await provider.exchange(code: "invalid_code") + XCTFail("Should have thrown error") + } catch { + XCTAssertNotNil(error as? UberAuthError) + XCTAssertEqual(mockTokenManager.saveTokenCallCount, 0) + } + } + + func test_launch_async_appNotInstalled_returnsFalse() async { + configurationProvider.isInstalledHandler = { _, _ in false } + + let provider = AuthorizationCodeAuthProvider( + configurationProvider: configurationProvider + ) + + let launched = await provider.launch(context: (.eats, nil)) + + XCTAssertFalse(launched) + } + + func test_launch_async_appInstalled_success_returnsTrue() async { + configurationProvider.isInstalledHandler = { _, _ in true } + + let mockLauncher = ApplicationLaunchingMock() + mockLauncher.launchResult = true + + let provider = AuthorizationCodeAuthProvider( + configurationProvider: configurationProvider, + applicationLauncher: mockLauncher + ) + + let launched = await provider.launch(context: (.eats, nil)) + + XCTAssertTrue(launched) + XCTAssertEqual(mockLauncher.launchUrlCallCount, 1) + } + + func test_launch_async_appInstalled_launchFails_returnsFalse() async { + configurationProvider.isInstalledHandler = { _, _ in true } + + let mockLauncher = ApplicationLaunchingMock() + mockLauncher.launchResult = false + + let provider = AuthorizationCodeAuthProvider( + configurationProvider: configurationProvider, + applicationLauncher: mockLauncher + ) + + let launched = await provider.launch(context: (.eats, nil)) + + XCTAssertFalse(launched) + XCTAssertEqual(mockLauncher.launchUrlCallCount, 1) + } + + func test_executeLogin_async_inApp_usesAuthenticationSession() async throws { + let authSessionBuilder: AuthorizationCodeAuthProvider.AuthenticationSessionBuilder = { _, _, _, completion in + let client = Client(authorizationCode: "in_app_code") + completion(.success(client)) + return AuthenticationSessioningMock() + } + + let provider = AuthorizationCodeAuthProvider( + authenticationSessionBuilder: authSessionBuilder, + shouldExchangeAuthCode: false, + configurationProvider: configurationProvider + ) + + let client = try await provider.executeLogin( + authDestination: .inApp, + requestURI: nil + ) + + XCTAssertEqual(client.authorizationCode, "in_app_code") + } + + func test_executeLogin_async_native_fallsBackToInApp() async throws { + configurationProvider.isInstalledHandler = { _, _ in true } + + let mockLauncher = ApplicationLaunchingMock() + mockLauncher.launchResult = false + + let authSessionBuilder: AuthorizationCodeAuthProvider.AuthenticationSessionBuilder = { _, _, _, completion in + let client = Client(authorizationCode: "fallback_code") + completion(.success(client)) + return AuthenticationSessioningMock() + } + + let provider = AuthorizationCodeAuthProvider( + authenticationSessionBuilder: authSessionBuilder, + shouldExchangeAuthCode: false, + configurationProvider: configurationProvider, + applicationLauncher: mockLauncher + ) + + let client = try await provider.executeLogin( + authDestination: .native(appPriority: [.eats, .driver]), + requestURI: nil + ) + + XCTAssertEqual(client.authorizationCode, "fallback_code") + XCTAssertEqual(mockLauncher.launchUrlCallCount, 2, "Should try both apps") + } + + func test_execute_async_withoutExchange_returnsAuthCode() async throws { + let authSessionBuilder: AuthorizationCodeAuthProvider.AuthenticationSessionBuilder = { _, _, _, completion in + completion(.success(Client(authorizationCode: "code"))) + return AuthenticationSessioningMock() + } + + let provider = AuthorizationCodeAuthProvider( + authenticationSessionBuilder: authSessionBuilder, + shouldExchangeAuthCode: false, + configurationProvider: configurationProvider + ) + + let client = try await provider.execute(authDestination: .inApp, prefill: nil) + + XCTAssertNotNil(client.authorizationCode) + XCTAssertNil(client.accessToken) + } + + func test_execute_async_withExchange_returnsToken() async throws { + let mockNetwork = NetworkProvidingMock() + mockNetwork.executeAsyncResult = .success(AccessToken( + tokenString: "token", + refreshToken: "refresh", + tokenType: "Bearer", + expiresIn: 3600, + scope: [] + )) + + let mockTokenManager = TokenManagingMock() + mockTokenManager.saveTokenHandler = { _, _, _ in true } + + let authSessionBuilder: AuthorizationCodeAuthProvider.AuthenticationSessionBuilder = { _, _, _, completion in + completion(.success(Client(authorizationCode: "code"))) + return AuthenticationSessioningMock() + } + + let provider = AuthorizationCodeAuthProvider( + authenticationSessionBuilder: authSessionBuilder, + shouldExchangeAuthCode: true, + configurationProvider: configurationProvider, + networkProvider: mockNetwork, + tokenManager: mockTokenManager + ) + + let client = try await provider.execute(authDestination: .inApp, prefill: nil) + + XCTAssertNotNil(client.accessToken) + XCTAssertEqual(mockNetwork.executeCallCount, 1) + } + + func test_execute_async_withPrefill_executesPAR() async throws { + let mockNetwork = NetworkProvidingMock() + mockNetwork.executeAsyncResult = .success(Par( + requestURI: "urn:par", + expiresIn: Date(timeIntervalSinceNow: 3600) + )) + + let authSessionBuilder: AuthorizationCodeAuthProvider.AuthenticationSessionBuilder = { _, _, _, completion in + completion(.success(Client(authorizationCode: "code"))) + return AuthenticationSessioningMock() + } + + let provider = AuthorizationCodeAuthProvider( + authenticationSessionBuilder: authSessionBuilder, + shouldExchangeAuthCode: false, + configurationProvider: configurationProvider, + networkProvider: mockNetwork + ) + + _ = try await provider.execute(authDestination: .inApp, prefill: Prefill(email: "test@test.com")) + + XCTAssertEqual(mockNetwork.executeCallCount, 1) + } + + func test_execute_async_authenticationError_throwsError() async { + let authSessionBuilder: AuthorizationCodeAuthProvider.AuthenticationSessionBuilder = { _, _, _, completion in + completion(.failure(UberAuthError.cancelled)) + return AuthenticationSessioningMock() + } + + let provider = AuthorizationCodeAuthProvider( + authenticationSessionBuilder: authSessionBuilder, + shouldExchangeAuthCode: false, + configurationProvider: configurationProvider + ) + + do { + _ = try await provider.execute(authDestination: .inApp, prefill: nil) + XCTFail("Should have thrown error") + } catch { + XCTAssertNotNil(error as? UberAuthError) + } + } + + func test_execute_async_exchangeError_throwsError() async { + let mockNetwork = NetworkProvidingMock() + mockNetwork.executeAsyncResult = .failure(UberAuthError.serviceError) + + let authSessionBuilder: AuthorizationCodeAuthProvider.AuthenticationSessionBuilder = { _, _, _, completion in + completion(.success(Client(authorizationCode: "code"))) + return AuthenticationSessioningMock() + } + + let provider = AuthorizationCodeAuthProvider( + authenticationSessionBuilder: authSessionBuilder, + shouldExchangeAuthCode: true, + configurationProvider: configurationProvider, + networkProvider: mockNetwork + ) + + do { + _ = try await provider.execute(authDestination: .inApp, prefill: nil) + XCTFail("Should have thrown error") + } catch { + XCTAssertNotNil(error as? UberAuthError) + } + } + + func test_execute_async_existingSession_throwsError() async { + let provider = AuthorizationCodeAuthProvider( + shouldExchangeAuthCode: false, + configurationProvider: configurationProvider + ) + provider.currentSession = AuthenticationSessioningMock() + + do { + _ = try await provider.execute(authDestination: .inApp, prefill: nil) + XCTFail("Should have thrown error") + } catch { + XCTAssertNotNil(error as? UberAuthError) + } + } + + // MARK: Environment + + func test_environment_production_usesProductionBaseUrl() { + var capturedUrl: URL? + let authenticationSessionBuilder: AuthorizationCodeAuthProvider.AuthenticationSessionBuilder = { _, _, url, _ in + capturedUrl = url + return AuthenticationSessioningMock() + } + + let provider = AuthorizationCodeAuthProvider( + authenticationSessionBuilder: authenticationSessionBuilder, + configurationProvider: configurationProvider, + environment: .production + ) + + provider.execute(authDestination: .inApp, completion: { _ in }) + + XCTAssertTrue(capturedUrl?.absoluteString.contains("auth.uber.com") == true) + XCTAssertFalse(capturedUrl?.absoluteString.contains("sandbox-login.uber.com") == true) + } + + func test_environment_sandbox_usesSandboxBaseUrl() { + var capturedUrl: URL? + let authenticationSessionBuilder: AuthorizationCodeAuthProvider.AuthenticationSessionBuilder = { _, _, url, _ in + capturedUrl = url + return AuthenticationSessioningMock() + } + + let provider = AuthorizationCodeAuthProvider( + authenticationSessionBuilder: authenticationSessionBuilder, + configurationProvider: configurationProvider, + environment: .sandbox + ) + + provider.execute(authDestination: .inApp, completion: { _ in }) + + XCTAssertTrue(capturedUrl?.absoluteString.contains("sandbox-login.uber.com") == true) + XCTAssertFalse(capturedUrl?.absoluteString.contains("auth.uber.com") == true) + } + + func test_environment_sandbox_nativeLogin_usesSandboxBaseUrl() { + configurationProvider.isInstalledHandler = { _, _ in true } + + let expectation = XCTestExpectation() + let applicationLauncher = ApplicationLaunchingMock() + applicationLauncher.launchHandler = { url, completion in + XCTAssertTrue(url.absoluteString.contains("sandbox-login.uber.com")) + expectation.fulfill() + completion?(true) + } + + let provider = AuthorizationCodeAuthProvider( + configurationProvider: configurationProvider, + applicationLauncher: applicationLauncher, + environment: .sandbox + ) + + provider.execute(authDestination: .native(appPriority: [.rides]), completion: { _ in }) + + wait(for: [expectation], timeout: 0.2) + } +} diff --git a/examples/UberSDK/UberSDKTests/UberAuth/AuthorizationCodeResponseParserTests.swift b/examples/UberSDK/UberSDKTests/UberAuth/AuthorizationCodeResponseParserTests.swift index f56a337..1adc6d8 100644 --- a/examples/UberSDK/UberSDKTests/UberAuth/AuthorizationCodeResponseParserTests.swift +++ b/examples/UberSDK/UberSDKTests/UberAuth/AuthorizationCodeResponseParserTests.swift @@ -105,14 +105,14 @@ final class AuthorizationCodeResponseParserTests: XCTestCase { } func test_parse_invalidUrl_returnsFailure() { - - let url = URL(filePath: "invalid_scheme://host?code=123") - + + let url = URL(string: "invalid-scheme://host")! + let result = responseParser( url: url ) - - XCTAssertEqual(result, .failure(UberAuthError.invalidResponse)) + + XCTAssertEqual(result, .failure(UberAuthError.invalidAuthCode)) } func test_parse_authorizationCodeParameter_returnsSuccess() { diff --git a/examples/UberSDK/UberSDKTests/UberRides/RideRequestViewTests.swift b/examples/UberSDK/UberSDKTests/UberRides/RideRequestViewTests.swift index 2b93c57..e4a7be0 100644 --- a/examples/UberSDK/UberSDKTests/UberRides/RideRequestViewTests.swift +++ b/examples/UberSDK/UberSDKTests/UberRides/RideRequestViewTests.swift @@ -48,9 +48,7 @@ class RideRequestViewTests: XCTestCase { let request = URLRequest(url: URL(string: "uberConnect://oauth#error=unauthorized")!) view.webView.load(request) - waitForExpectations(timeout: timeout, handler: { error in - XCTAssertNil(error) - }) + waitForExpectations(timeout: timeout) } /**