diff --git a/Example/AuthViewController.swift b/Example/AuthViewController.swift index c946e60cee1..9c8de4bc98d 100644 --- a/Example/AuthViewController.swift +++ b/Example/AuthViewController.swift @@ -46,8 +46,9 @@ final class AuthViewController: UIViewController { showProgress() oktaAppAuth?.authenticate(withSessionToken: token) { authStateManager, error in self.hideProgress() + if let error = error { - self.presentError(error) + self.showMessage(error) return } @@ -56,8 +57,12 @@ final class AuthViewController: UIViewController { } } - func presentError(_ error: Error) { - self.showMessage("Error: \(error.localizedDescription)") + func showMessage(_ error: Error) { + if let oidcError = error as? OktaOidcError { + messageView.text = oidcError.displayMessage + } else { + messageView.text = error.localizedDescription + } } func showMessage(_ message: String) { diff --git a/Example/ViewController.swift b/Example/ViewController.swift index 55b1773d06a..f38fff96d9c 100644 --- a/Example/ViewController.swift +++ b/Example/ViewController.swift @@ -65,7 +65,7 @@ final class ViewController: UIViewController { super.viewWillAppear(animated) guard oktaAppAuth != nil else { - self.updateUI(updateText: "SDK is not configured!") + self.showMessage("SDK is not configured!") return } @@ -103,14 +103,14 @@ final class ViewController: UIViewController { @IBAction func userInfoButton(_ sender: Any) { authStateManager?.getUser() { response, error in if let error = error { - self.updateUI(updateText: "Error: \(error)") + self.showMessage(error) return } if response != nil { var userInfoText = "" response?.forEach { userInfoText += ("\($0): \($1) \n") } - self.updateUI(updateText: userInfoText) + self.showMessage(userInfoText) } } } @@ -121,11 +121,11 @@ final class ViewController: UIViewController { authStateManager?.introspect(token: accessToken, callback: { payload, error in guard let isValid = payload?["active"] as? Bool else { - self.updateUI(updateText: "Error: \(error?.localizedDescription ?? "Unknown")") + self.showMessage("Error: \(error?.localizedDescription ?? "Unknown")") return } - self.updateUI(updateText: "Is the AccessToken valid? - \(isValid)") + self.showMessage("Is the AccessToken valid? - \(isValid)") }) } @@ -134,8 +134,8 @@ final class ViewController: UIViewController { guard let accessToken = authStateManager?.accessToken else { return } authStateManager?.revoke(accessToken) { _, error in - if error != nil { self.updateUI(updateText: "Error: \(error!)") } - self.updateUI(updateText: "AccessToken was revoked") + if error != nil { self.showMessage("Error: \(error!)") } + self.showMessage("AccessToken was revoked") } } @@ -143,7 +143,7 @@ final class ViewController: UIViewController { oktaAppAuth?.signInWithBrowser(from: self) { authStateManager, error in if let error = error { self.authStateManager = nil - self.updateUI(updateText: "Error: \(error)") + self.showMessage("Error: \(error.localizedDescription)") return } @@ -158,9 +158,9 @@ final class ViewController: UIViewController { oktaAppAuth?.signOut(authStateManager: authStateManager, from: self, progressHandler: { currentOption in switch currentOption { case .revokeAccessToken, .revokeRefreshToken, .removeTokensFromStorage, .revokeTokensOptions: - self.updateUI(updateText: "Revoking tokens...") + self.showMessage("Revoking tokens...") case .signOutFromOkta: - self.updateUI(updateText: "Signing out from Okta...") + self.showMessage("Signing out from Okta...") default: break } @@ -169,13 +169,21 @@ final class ViewController: UIViewController { self.authStateManager = nil self.buildTokenTextView() } else { - self.updateUI(updateText: "Error: failed to logout") + self.showMessage("Error: failed to logout") } }) } - - func updateUI(updateText: String) { - tokenView.text = updateText + + func showMessage(_ message: String) { + tokenView.text = message + } + + func showMessage(_ error: Error) { + if let oidcError = error as? OktaOidcError { + tokenView.text = oidcError.displayMessage + } else { + tokenView.text = error.localizedDescription + } } func buildTokenTextView() { @@ -197,7 +205,7 @@ final class ViewController: UIViewController { tokenString += "\nRefresh Token: \(refreshToken)\n" } - self.updateUI(updateText: tokenString) + self.showMessage(tokenString) } } @@ -215,3 +223,30 @@ extension ViewController: OktaNetworkRequestCustomizationDelegate { } } } + +extension OktaOidcError { + var displayMessage: String { + switch self { + case let .api(message, _): + switch (self as NSError).code { + case NSURLErrorNotConnectedToInternet, + NSURLErrorNetworkConnectionLost, + NSURLErrorCannotLoadFromNetwork, + NSURLErrorCancelled: + return "No Internet Connection" + case NSURLErrorTimedOut: + return "Connection timed out" + default: + break + } + + return "API Error occurred: \(message)" + case let .authorization(error, _): + return "Authorization error: \(error)" + case let .unexpectedAuthCodeResponse(statusCode): + return "Authorization failed due to incorrect status code: \(statusCode)" + default: + return localizedDescription + } + } +} diff --git a/README.md b/README.md index 6de74137e98..bbce9f5f43b 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,7 @@ You can learn more on the [Okta + iOS](https://developer.okta.com/code/ios/) pag - [Development](#development) - [Running Tests](#running-tests) - [Modify network requests](#modify-network-requests) +- [Migration](#migration) - [Known issues](#known-issues) - [Contributing](#contributing) @@ -507,6 +508,20 @@ extension SomeNSObject: OktaNetworkRequestCustomizationDelegate { ***Note:*** It is highly recommended to copy all of the existing parameters from the original URLRequest object to modified request without any changes. Altering of this data could lead network request to fail. If `customizableURLRequest(_:)` method returns `nil` default request will be used. +## Migration + +### Migrating from 3.10.x to 3.11.x + +The SDK `okta-oidc-ios` has a major changes in error handling. Consider these guidelines to update your code. + +- `APIError` is renamed as `api`. +- `api` error has the additional parameter `underlyingError`, it's an optional and indicates the origin of the error. +- Introduced a new error `authorization(error:description:)`. +- `authorization` error appears when authorization server fails due to errors during authorization. +- `unexpectedAuthCodeResponse(statusCode:)` has an error code parameter. +- `OktaOidcError` conforms to `CustomNSError` protocol. It means you can convert the error to `NSError` and get `code`, `userInfo`, `domain`, `underlyingErrors`. +- `OktaOidcError` conforms to `Equatable` protocol. The errors can be compared for equality using the operator `==` or inequality using the operator `!=`. + ## Known issues ### iOS shows permission dialog(`{App} Wants to Use {Auth Domain} to Sign In`) for Okta Sign Out flows diff --git a/Sources/OktaOidc/Common/Internal/OIDAuthState+Okta.swift b/Sources/OktaOidc/Common/Internal/OIDAuthState+Okta.swift index 4121b959deb..3dbe269c942 100644 --- a/Sources/OktaOidc/Common/Internal/OIDAuthState+Okta.swift +++ b/Sources/OktaOidc/Common/Internal/OIDAuthState+Okta.swift @@ -17,29 +17,37 @@ import OktaOidc_AppAuth // Okta Extension of OIDAuthState extension OKTAuthState { - static func getState(withAuthRequest authRequest: OKTAuthorizationRequest, delegate: OktaNetworkRequestCustomizationDelegate? = nil, callback: @escaping (OKTAuthState?, OktaOidcError?) -> Void ) { + static func getState(withAuthRequest authRequest: OKTAuthorizationRequest, delegate: OktaNetworkRequestCustomizationDelegate? = nil, callback finalize: @escaping (OKTAuthState?, OktaOidcError?) -> Void ) { - let finalize: ((OKTAuthState?, OktaOidcError?) -> Void) = { state, error in - callback(state, error) - } - // Make authCode request - OKTAuthorizationService.perform(authRequest: authRequest, delegate: delegate, callback: { authResponse, error in + OKTAuthorizationService.perform(authRequest: authRequest, delegate: delegate) { authResponse, error in guard let authResponse = authResponse else { - finalize(nil, OktaOidcError.APIError("Authorization Error: \(error?.localizedDescription ?? "No authentication response.")")) + finalize(nil, .api(message: "Authorization Error: \(error?.localizedDescription ?? "No authentication response.")", underlyingError: error)) return } - + + if let oauthError = authResponse.additionalParameters?[OKTOAuthErrorFieldError] as? String { + let oauthErrorDescription = authResponse.additionalParameters?[OKTOAuthErrorFieldErrorDescription] as? String + finalize(nil, .authorization(error: oauthError, description: oauthErrorDescription)) + return + } + guard authResponse.authorizationCode != nil, let tokenRequest = authResponse.tokenExchangeRequest() else { - finalize(nil, OktaOidcError.unableToGetAuthCode) + finalize(nil, .unableToGetAuthCode) return } // Make token request - OKTAuthorizationService.perform(tokenRequest, originalAuthorizationResponse: authResponse, delegate: delegate, callback: { tokenResponse, error in + OKTAuthorizationService.perform(tokenRequest, originalAuthorizationResponse: authResponse, delegate: delegate) { tokenResponse, error in guard let tokenResponse = tokenResponse else { - finalize(nil, OktaOidcError.APIError("Authorization Error: \(error?.localizedDescription ?? "No token response.")")) + finalize(nil, OktaOidcError.api(message: "Authorization Error: \(error?.localizedDescription ?? "No token response.")", underlyingError: error)) + return + } + + if let oauthError = tokenResponse.additionalParameters?[OKTOAuthErrorFieldError] as? String { + let oauthErrorDescription = tokenResponse.additionalParameters?[OKTOAuthErrorFieldErrorDescription] as? String + finalize(nil, .authorization(error: oauthError, description: oauthErrorDescription)) return } @@ -48,7 +56,7 @@ extension OKTAuthState { registrationResponse: nil, delegate: delegate) finalize(authState, nil) - }) - }) + } + } } } diff --git a/Sources/OktaOidc/Common/Internal/OIDAuthorizationService+Okta.swift b/Sources/OktaOidc/Common/Internal/OIDAuthorizationService+Okta.swift index 4531ec61d05..71062b983c6 100644 --- a/Sources/OktaOidc/Common/Internal/OIDAuthorizationService+Okta.swift +++ b/Sources/OktaOidc/Common/Internal/OIDAuthorizationService+Okta.swift @@ -35,33 +35,43 @@ extension OKTAuthorizationService { delegate?.didReceive(response) guard let response = response as? HTTPURLResponse else { - callback(nil, error) + callback(nil, OktaOidcError.api(message: "Authentication Error: No response", underlyingError: error)) return } - guard response.statusCode == 302, - let locationHeader = response.allHeaderFields["Location"] as? String, - let urlComonents = URLComponents(string: locationHeader), - let queryItems = urlComonents.queryItems else { - callback(nil, OktaOidcError.unexpectedAuthCodeResponse) - return + guard response.statusCode == 302 else { + callback(nil, OktaOidcError.unexpectedAuthCodeResponse(statusCode: response.statusCode)) + return } + + guard let locationHeader = response.allHeaderFields["Location"] as? String, + let urlComponents = URLComponents(string: locationHeader), + let queryItems = urlComponents.queryItems else { + callback(nil, OktaOidcError.noLocationHeader) + return + } - var parameters = [String: NSString]() - queryItems.forEach({ item in + var parameters: [String: NSString] = [:] + queryItems.forEach { item in parameters[item.name] = item.value as NSString? - }) - - if let allHeaderFields = response.allHeaderFields as? [String: String], - let url = response.url { - let httpCookies = HTTPCookie.cookies(withResponseHeaderFields: allHeaderFields, for: url) - for cookie in httpCookies { - HTTPCookieStorage.shared.setCookie(cookie) - } } - + let authResponse = OKTAuthorizationResponse(request: authRequest, parameters: parameters) + + setCookie(from: response) + callback(authResponse, error) }.resume() } + + private static func setCookie(from response: HTTPURLResponse) { + guard let allHeaderFields = response.allHeaderFields as? [String: String], + let url = response.url else { + return + } + + HTTPCookie.cookies(withResponseHeaderFields: allHeaderFields, for: url).forEach { + HTTPCookieStorage.shared.setCookie($0) + } + } } diff --git a/Sources/OktaOidc/Common/Internal/OktaOidcRestApi.swift b/Sources/OktaOidc/Common/Internal/OktaOidcRestApi.swift index 717c0cacb37..90eb512ab57 100644 --- a/Sources/OktaOidc/Common/Internal/OktaOidcRestApi.swift +++ b/Sources/OktaOidc/Common/Internal/OktaOidcRestApi.swift @@ -28,14 +28,14 @@ class OktaOidcRestApi: OktaOidcHttpApiProtocol { let httpResponse = response as? HTTPURLResponse else { let errorMessage = error?.localizedDescription ?? "No response data" DispatchQueue.main.async { - onError(OktaOidcError.APIError(errorMessage)) + onError(OktaOidcError.api(message: errorMessage, underlyingError: error)) } return } guard 200 ..< 300 ~= httpResponse.statusCode else { DispatchQueue.main.async { - onError(OktaOidcError.APIError(HTTPURLResponse.localizedString(forStatusCode: httpResponse.statusCode))) + onError(OktaOidcError.api(message: HTTPURLResponse.localizedString(forStatusCode: httpResponse.statusCode), underlyingError: nil)) } return } diff --git a/Sources/OktaOidc/Common/Internal/Tasks/OktaOidcBrowserTask.swift b/Sources/OktaOidc/Common/Internal/Tasks/OktaOidcBrowserTask.swift index 9280d4df0a3..d3844c33a9e 100644 --- a/Sources/OktaOidc/Common/Internal/Tasks/OktaOidcBrowserTask.swift +++ b/Sources/OktaOidc/Common/Internal/Tasks/OktaOidcBrowserTask.swift @@ -40,7 +40,7 @@ class OktaOidcBrowserTask: OktaOidcTask { responseType: OKTResponseTypeCode, additionalParameters: self.config.additionalParams) guard let externalUserAgent = self.externalUserAgent() else { - callback(nil, OktaOidcError.APIError("Authorization Error: \(error?.localizedDescription ?? "No external User Agent.")")) + callback(nil, OktaOidcError.api(message: "Authorization Error: \(error?.localizedDescription ?? "No external User Agent.")", underlyingError: nil)) return } @@ -49,17 +49,22 @@ class OktaOidcBrowserTask: OktaOidcTask { delegate: delegate) { authorizationResponse, error in defer { self.userAgentSession = nil } - guard let authResponse = authorizationResponse else { - guard let error = error else { - return callback(nil, OktaOidcError.APIError("Authorization Error")) - } - if (error as NSError).code == OKTErrorCode.userCanceledAuthorizationFlow.rawValue { - return callback(nil, OktaOidcError.userCancelledAuthorizationFlow) - } else { - return callback(nil, OktaOidcError.APIError("Authorization Error: \(error.localizedDescription)")) - } + if let authResponse = authorizationResponse { + callback(authResponse, nil) + return } - callback(authResponse, nil) + + guard let error = error else { + callback(nil, OktaOidcError.api(message: "Authorization Error: No authorization response", underlyingError: nil)) + return + } + + if (error as NSError).code == OKTErrorCode.userCanceledAuthorizationFlow.rawValue { + callback(nil, OktaOidcError.userCancelledAuthorizationFlow) + return + } + + return callback(nil, OktaOidcError.api(message: "Authorization Error: \(error.localizedDescription)", underlyingError: error)) } self.userAgentSession = userAgentSession } @@ -83,7 +88,7 @@ class OktaOidcBrowserTask: OktaOidcTask { postLogoutRedirectURL: successRedirectURL, additionalParameters: self.config.additionalParams) guard let externalUserAgent = self.externalUserAgent() else { - callback(nil, OktaOidcError.APIError("Authorization Error: \(error?.localizedDescription ?? "No external User Agent.")")) + callback(nil, OktaOidcError.api(message: "Authorization Error: \(error?.localizedDescription ?? "No external User Agent.")", underlyingError: nil)) return } @@ -92,7 +97,7 @@ class OktaOidcBrowserTask: OktaOidcTask { var error: OktaOidcError? if let responseError = responseError { - error = OktaOidcError.APIError("Sign Out Error: \(responseError.localizedDescription)") + error = OktaOidcError.api(message: "Sign Out Error: \(responseError.localizedDescription)", underlyingError: nil) } callback((), error) diff --git a/Sources/OktaOidc/Common/Internal/Tasks/OktaOidcTask.swift b/Sources/OktaOidc/Common/Internal/Tasks/OktaOidcTask.swift index 0e391d0e4bd..97587844428 100644 --- a/Sources/OktaOidc/Common/Internal/Tasks/OktaOidcTask.swift +++ b/Sources/OktaOidc/Common/Internal/Tasks/OktaOidcTask.swift @@ -39,10 +39,7 @@ class OktaOidcTask { callback(OKTServiceConfiguration(discoveryDocument: oidConfig), nil) }, onError: { error in - let responseError = - "Error returning discovery document: \(error.localizedDescription). Please" + - " check your PList configuration" - callback(nil, OktaOidcError.APIError(responseError)) + callback(nil, error) }) } } diff --git a/Sources/OktaOidc/Common/OktaOidcError.swift b/Sources/OktaOidc/Common/OktaOidcError.swift index fd143bb107d..e249881fc2f 100644 --- a/Sources/OktaOidc/Common/OktaOidcError.swift +++ b/Sources/OktaOidc/Common/OktaOidcError.swift @@ -12,11 +12,18 @@ import Foundation -public enum OktaOidcError: Error { - case APIError(String) +public enum OktaOidcError: CustomNSError { + + /// See [RFC6749 Error Response](https://tools.ietf.org/html/rfc6749#section-4.1.2.1). + case authorization(error: String, description: String?) + + case api(message: String, underlyingError: Error?) + case unexpectedAuthCodeResponse(statusCode: Int) case errorFetchingFreshTokens(String) - case JWTDecodeError case JWTValidationError(String) + case redirectServerError(String) + case JWTDecodeError + case noLocationHeader case missingConfigurationValues case noBearerToken case noDiscoveryEndpoint @@ -30,17 +37,53 @@ public enum OktaOidcError: Error { case noUserInfoEndpoint case parseFailure case missingIdToken - case unexpectedAuthCodeResponse case userCancelledAuthorizationFlow case unableToGetAuthCode - case redirectServerError(String) + + public static var errorDomain: String = "\(Self.self)" + + /// Most of errors returns the general error code. + /// Error like `api`, `unexpectedAuthCodeResponse` return specific error code. + /// `api` returns the general error code if `underlyingError` is absent. + private static let generalErrorCode = -1012009 + + public var errorCode: Int { + switch self { + case let .api(_, underlyingError): + return (underlyingError as NSError?)?.code ?? Self.generalErrorCode + + case let .unexpectedAuthCodeResponse(statusCode): + return statusCode + default: + return Self.generalErrorCode + } + } + + public var errorUserInfo: [String: Any] { + var result: [String: Any] = [:] + result[NSLocalizedDescriptionKey] = errorDescription + + switch self { + case let .api(_, underlyingError): + result[NSUnderlyingErrorKey] = underlyingError + return result + default: + return result + } + } +} + +extension OktaOidcError: Equatable { + public static func == (lhs: OktaOidcError, rhs: OktaOidcError) -> Bool { + lhs as NSError == rhs as NSError + } } extension OktaOidcError: LocalizedError { public var errorDescription: String? { switch self { - case .APIError(error: let error): - return NSLocalizedString(error, comment: "") + case let .api(message, _): + return NSLocalizedString(message, comment: "") case .errorFetchingFreshTokens(error: let error): return NSLocalizedString("Error fetching fresh tokens: \(error)", comment: "") case .JWTDecodeError: @@ -74,14 +117,18 @@ extension OktaOidcError: LocalizedError { return NSLocalizedString("Failed to parse and/or convert object.", comment: "") case .missingIdToken: return NSLocalizedString("ID token needed to fulfill this operation.", comment: "") - case .unexpectedAuthCodeResponse: - return NSLocalizedString("Unexpected response format while retrieving authorization code.", comment: "") + case .unexpectedAuthCodeResponse(let statusCode): + return NSLocalizedString("Unexpected response format while retrieving authorization code. Status code: \(statusCode)", comment: "") case .userCancelledAuthorizationFlow: return NSLocalizedString("User cancelled current session", comment: "") case .unableToGetAuthCode: return NSLocalizedString("Unable to get authorization code.", comment: "") case .redirectServerError(error: let error): return NSLocalizedString(error, comment: "") + case let .authorization(error, description): + return NSLocalizedString("The authorization request failed due to \(error): \(description ?? "")", comment: "") + case .noLocationHeader: + return NSLocalizedString("Unable to get location header.", comment: "") } } } diff --git a/Tests/OktaOidcTests/OktaOidcDiscoveryTaskTests.swift b/Tests/OktaOidcTests/OktaOidcDiscoveryTaskTests.swift index 65538492f50..75398768604 100644 --- a/Tests/OktaOidcTests/OktaOidcDiscoveryTaskTests.swift +++ b/Tests/OktaOidcTests/OktaOidcDiscoveryTaskTests.swift @@ -48,14 +48,12 @@ class OktaOidcDiscoveryTaskTests: XCTestCase { } func testRunApiError() { - apiMock.configure(error: OktaOidcError.APIError("Test Error")) + let mockError = OktaOidcError.api(message: "Test Error", underlyingError: nil) + apiMock.configure(error: mockError) runAndWaitDiscovery(config: validConfig) { oidConfig, error in XCTAssertNil(oidConfig) - XCTAssertEqual( - "Error returning discovery document: Test Error. Please check your PList configuration", - error?.localizedDescription - ) + XCTAssertEqual(mockError, error as OktaOidcError?) } } diff --git a/Tests/OktaOidcTests/OktaOidcEndpointTests.swift b/Tests/OktaOidcTests/OktaOidcEndpointTests.swift index ef71f7fadf0..d222160c474 100644 --- a/Tests/OktaOidcTests/OktaOidcEndpointTests.swift +++ b/Tests/OktaOidcTests/OktaOidcEndpointTests.swift @@ -126,18 +126,18 @@ class OktaOidcEndpointTests: XCTestCase { func testNoEndpointError() { XCTAssertEqual( - OktaOidcError.noIntrospectionEndpoint.localizedDescription, - OktaOidcEndpoint.introspection.noEndpointError.localizedDescription + OktaOidcError.noIntrospectionEndpoint, + OktaOidcEndpoint.introspection.noEndpointError ) XCTAssertEqual( - OktaOidcError.noRevocationEndpoint.localizedDescription, - OktaOidcEndpoint.revocation.noEndpointError.localizedDescription + OktaOidcError.noRevocationEndpoint, + OktaOidcEndpoint.revocation.noEndpointError ) XCTAssertEqual( - OktaOidcError.noUserInfoEndpoint.localizedDescription, - OktaOidcEndpoint.userInfo.noEndpointError.localizedDescription + OktaOidcError.noUserInfoEndpoint, + OktaOidcEndpoint.userInfo.noEndpointError ) } diff --git a/Tests/OktaOidcTests/OktaOidcErrorTests.swift b/Tests/OktaOidcTests/OktaOidcErrorTests.swift new file mode 100644 index 00000000000..788fdc430f4 --- /dev/null +++ b/Tests/OktaOidcTests/OktaOidcErrorTests.swift @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2021-Present, Okta, Inc. and/or its affiliates. All rights reserved. + * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.") + * + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + * See the License for the specific language governing permissions and limitations under the License. + */ + +// swiftlint:disable force_try +// swiftlint:disable force_cast +// swiftlint:disable force_unwrapping + +@testable import OktaOidc +import XCTest + +#if SWIFT_PACKAGE +@testable import TestCommon +#endif + +final class OktaOidcErrorTests: XCTestCase { + + func testGeneralOidcError() { + let error = OktaOidcError.JWTDecodeError as NSError + + XCTAssertEqual(error.code, OktaOidcError.generalErrorCode) + XCTAssertEqual(error.domain, OktaOidcError.errorDomain) + XCTAssertEqual(error.userInfo[NSLocalizedDescriptionKey] as! String, error.localizedDescription) + XCTAssertNil(error.userInfo[NSUnderlyingErrorKey] as? NSError) + + if #available(iOS 14.5, *) { + XCTAssertTrue(error.underlyingErrors.isEmpty) + } + } + + func testApiError() { + let underlyingError = NSError(domain: NSURLErrorDomain, + code: NSURLErrorNetworkConnectionLost, + userInfo: [NSLocalizedDescriptionKey: "Localization error description"]) + let error = OktaOidcError.api(message: "Mock error", underlyingError: underlyingError) as NSError + + XCTAssertEqual(error.domain, OktaOidcError.errorDomain) + XCTAssertEqual(error.code, underlyingError.code) + XCTAssertEqual(error.userInfo[NSUnderlyingErrorKey] as! NSError, underlyingError) + XCTAssertEqual(error.userInfo[NSLocalizedDescriptionKey] as! String, error.localizedDescription) + + if #available(iOS 14.5, *) { + XCTAssertEqual(error.underlyingErrors.first! as NSError, underlyingError) + } + } + + func testApiErrorWithoutUnderlyingError() { + let error = OktaOidcError.api(message: "Mock error", underlyingError: nil) as NSError + + XCTAssertEqual(error.domain, OktaOidcError.errorDomain) + XCTAssertEqual(error.code, OktaOidcError.generalErrorCode) + XCTAssertNil(error.userInfo[NSUnderlyingErrorKey] as? NSError) + XCTAssertEqual(error.userInfo[NSLocalizedDescriptionKey] as! String, error.localizedDescription) + + if #available(iOS 14.5, *) { + XCTAssertTrue(error.underlyingErrors.isEmpty) + } + } + + func testAuthCodeResponseError() { + let error = OktaOidcError.unexpectedAuthCodeResponse(statusCode: 404) as NSError + + XCTAssertEqual(error.domain, OktaOidcError.errorDomain) + XCTAssertEqual(error.code, 404) + XCTAssertNil(error.userInfo[NSUnderlyingErrorKey] as? NSError) + XCTAssertEqual(error.userInfo[NSLocalizedDescriptionKey] as! String, error.localizedDescription) + } + + func testErrorsEquatability() { + // unexpectedAuthCodeResponse + let lhsAuthCodeResponse = OktaOidcError.unexpectedAuthCodeResponse(statusCode: NSURLErrorCannotLoadFromNetwork) + let rhsAuthCodeResponse = OktaOidcError.unexpectedAuthCodeResponse(statusCode: NSURLErrorCannotLoadFromNetwork) + + XCTAssertEqual(lhsAuthCodeResponse, rhsAuthCodeResponse) + XCTAssertEqual(lhsAuthCodeResponse as NSError, rhsAuthCodeResponse as NSError) + XCTAssertNotEqual(lhsAuthCodeResponse, .unexpectedAuthCodeResponse(statusCode: 123)) + XCTAssertNotEqual(lhsAuthCodeResponse, .noPListGiven) + + // api + let underlyingError = NSError(domain: NSURLErrorDomain, + code: NSURLErrorNetworkConnectionLost, + userInfo: [NSLocalizedDescriptionKey: "Localization error description"]) + let lhsApiError = OktaOidcError.api(message: "Mock error", underlyingError: underlyingError) + let rhsApiError = OktaOidcError.api(message: "Mock error", underlyingError: underlyingError) + + XCTAssertEqual(lhsApiError, rhsApiError) + XCTAssertEqual(lhsApiError as NSError, rhsApiError as NSError) + XCTAssertNotEqual(lhsApiError, .api(message: "Mock error", underlyingError: nil)) + XCTAssertNotEqual(lhsApiError, .JWTDecodeError) + + // authorization + let rhsAuthorization = OktaOidcError.authorization(error: "Error", description: "Localized Description") + let lhsAuthorization = OktaOidcError.authorization(error: "Error", description: "Localized Description") + + XCTAssertEqual(rhsAuthorization, lhsAuthorization) + XCTAssertEqual(rhsAuthorization as NSError, lhsAuthorization as NSError) + XCTAssertNotEqual(rhsAuthorization, .authorization(error: "Error", description: "Localized Description (2)")) + XCTAssertNotEqual(.missingConfigurationValues, lhsAuthorization) + + // errorFetchingFreshTokens + let rhsFetchingFreshTokens = OktaOidcError.errorFetchingFreshTokens("Fetch Error") + let lhsFetchingFreshTokens = OktaOidcError.errorFetchingFreshTokens("Fetch Error") + XCTAssertEqual(rhsFetchingFreshTokens, lhsFetchingFreshTokens) + XCTAssertEqual(rhsFetchingFreshTokens as NSError, lhsFetchingFreshTokens as NSError) + XCTAssertNotEqual(rhsFetchingFreshTokens, .errorFetchingFreshTokens("Fetch Error (2)")) + XCTAssertNotEqual(.noBearerToken, rhsFetchingFreshTokens) + } +} diff --git a/Tests/OktaOidcTests/OktaOidcStateManagerTests.swift b/Tests/OktaOidcTests/OktaOidcStateManagerTests.swift index df0c7a98d6b..fc067b417d5 100644 --- a/Tests/OktaOidcTests/OktaOidcStateManagerTests.swift +++ b/Tests/OktaOidcTests/OktaOidcStateManagerTests.swift @@ -78,16 +78,19 @@ class OktaOidcStateManagerTests: XCTestCase { func testIntrospectFailed() { // Mock REST API calls - apiMock.configure(error: .APIError("Test Error")) + let underlyingError = NSError(domain: NSURLErrorDomain, + code: NSURLErrorNetworkConnectionLost, + userInfo: [NSLocalizedDescriptionKey: "Localization error description"]) + let mockError = OktaOidcError.api(message: "Test Error", underlyingError: underlyingError) + apiMock.configure(error: mockError) let introspectExpectation = expectation(description: "Will fail with error.") authStateManager.introspect(token: authStateManager.accessToken) { payload, error in XCTAssertNil(payload) - XCTAssertEqual( - OktaOidcError.APIError("Test Error").localizedDescription, - error?.localizedDescription - ) + + XCTAssertEqual(mockError, error as? OktaOidcError) + introspectExpectation.fulfill() } @@ -112,16 +115,19 @@ class OktaOidcStateManagerTests: XCTestCase { func testRevokeNoBearerToken() { // Mock REST API calls - apiMock.configure(error: .APIError("Test Error")) + let underlyingError = NSError(domain: NSURLErrorDomain, + code: NSURLErrorNetworkConnectionLost, + userInfo: [NSLocalizedDescriptionKey: "Localization error description"]) + + let mockError = OktaOidcError.api(message: "Test Error", underlyingError: underlyingError) + apiMock.configure(error: mockError) let revokeExpectation = expectation(description: "Will fail with error.") authStateManager.revoke(nil) { isRevoked, error in XCTAssertFalse(isRevoked) - XCTAssertEqual( - OktaOidcError.noBearerToken.localizedDescription, - error?.localizedDescription - ) + + XCTAssertEqual(OktaOidcError.noBearerToken, error as? OktaOidcError) revokeExpectation.fulfill() } @@ -131,16 +137,18 @@ class OktaOidcStateManagerTests: XCTestCase { func testRevokeFailed() { // Mock REST API calls - apiMock.configure(error: .APIError("Test Error")) + let underlyingError = NSError(domain: NSURLErrorDomain, + code: NSURLErrorNetworkConnectionLost, + userInfo: [NSLocalizedDescriptionKey: "Localization error description"]) + + let mockError = OktaOidcError.api(message: "Test Error", underlyingError: underlyingError) + apiMock.configure(error: mockError) let revokeExpectation = expectation(description: "Will fail with error.") authStateManager.revoke(authStateManager.accessToken) { isRevoked, error in XCTAssertFalse(isRevoked) - XCTAssertEqual( - OktaOidcError.APIError("Test Error").localizedDescription, - error?.localizedDescription - ) + XCTAssertEqual(mockError, error as? OktaOidcError) revokeExpectation.fulfill() } @@ -166,16 +174,18 @@ class OktaOidcStateManagerTests: XCTestCase { func testGetUserFailed() { // Mock REST API calls - apiMock.configure(error: .APIError("Test Error")) + let underlyingError = NSError(domain: NSURLErrorDomain, + code: NSURLErrorNetworkConnectionLost, + userInfo: [NSLocalizedDescriptionKey: "Localization error description"]) + + let mockError = OktaOidcError.api(message: "Test Error", underlyingError: underlyingError) + apiMock.configure(error: mockError) let userInfoExpectation = expectation(description: "Will fail with error.") authStateManager.getUser { payload, error in XCTAssertNil(payload) - XCTAssertEqual( - OktaOidcError.APIError("Test Error").localizedDescription, - error?.localizedDescription - ) + XCTAssertEqual(mockError, error as? OktaOidcError) userInfoExpectation.fulfill() } @@ -192,8 +202,8 @@ class OktaOidcStateManagerTests: XCTestCase { authStateManager.getUser { payload, error in XCTAssertNil(payload) XCTAssertEqual( - OktaOidcError.noBearerToken.localizedDescription, - error?.localizedDescription + OktaOidcError.noBearerToken, + error as? OktaOidcError ) userInfoExpectation.fulfill() diff --git a/okta-oidc.xcodeproj/project.pbxproj b/okta-oidc.xcodeproj/project.pbxproj index a53cb59b75f..18e70f16ae2 100644 --- a/okta-oidc.xcodeproj/project.pbxproj +++ b/okta-oidc.xcodeproj/project.pbxproj @@ -39,6 +39,7 @@ 92AF826326209E1C004D157D /* OktaOidcHttpApiProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 922628782617553E002F6BC4 /* OktaOidcHttpApiProtocol.swift */; }; 92B62A2D25C41E59002CE64F /* OKTTokensAuthMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92B62A2C25C41E59002CE64F /* OKTTokensAuthMock.swift */; }; 92B62A2E25C41E59002CE64F /* OKTTokensAuthMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92B62A2C25C41E59002CE64F /* OKTTokensAuthMock.swift */; }; + 92DB056B2751129E00B3714F /* OktaOidcErrorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92DB05692751125C00B3714F /* OktaOidcErrorTests.swift */; }; 9601C34C256DD14800C084F5 /* OktaRedirectServerConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A167889D2433C7B500D1651D /* OktaRedirectServerConfigurationTests.swift */; }; 9601C34D256DD14800C084F5 /* OktaOidcBrowserTaskMACTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A167889F2433D0DB00D1651D /* OktaOidcBrowserTaskMACTests.swift */; }; 9601C34E256DD14800C084F5 /* OktaOidcSignOutHandlerMACTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A16788A62435250700D1651D /* OktaOidcSignOutHandlerMACTests.swift */; }; @@ -377,6 +378,7 @@ 922628782617553E002F6BC4 /* OktaOidcHttpApiProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OktaOidcHttpApiProtocol.swift; sourceTree = ""; }; 922628DA261B1F90002F6BC4 /* XCUIElement+Utils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "XCUIElement+Utils.swift"; sourceTree = ""; }; 92B62A2C25C41E59002CE64F /* OKTTokensAuthMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OKTTokensAuthMock.swift; sourceTree = ""; }; + 92DB05692751125C00B3714F /* OktaOidcErrorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OktaOidcErrorTests.swift; sourceTree = ""; }; 960961F925672EC40077978A /* Package.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = ""; }; 9671A102256F153900D0B03F /* OktaOidcBrowserProtocolIOS.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = OktaOidcBrowserProtocolIOS.swift; path = Internal/OktaOidcBrowserProtocolIOS.swift; sourceTree = ""; }; 9671A103256F153900D0B03F /* OktaOidc+BrowserIOS.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "OktaOidc+BrowserIOS.swift"; sourceTree = ""; }; @@ -598,9 +600,9 @@ 2F32CA1F229D38D4003A6768 /* Products */, 2F32CBD2229D42D0003A6768 /* Frameworks */, ); - indentWidth = 2; + indentWidth = 4; sourceTree = ""; - tabWidth = 2; + tabWidth = 4; }; 2F32CA1F229D38D4003A6768 /* Products */ = { isa = PBXGroup; @@ -784,6 +786,7 @@ A16788B72436AA2C00D1651D /* OktaOidcTests */ = { isa = PBXGroup; children = ( + 92DB05692751125C00B3714F /* OktaOidcErrorTests.swift */, 2F32CC09229D4CF8003A6768 /* OktaOidcTests.swift */, 2F32CC0A229D4CF8003A6768 /* OktaOidcStateManagerTests.swift */, 2F32CBE6229D4CF8003A6768 /* OktaOidcEndpointTests.swift */, @@ -1432,6 +1435,7 @@ DEBFB8E42507A7C500A27026 /* OIDAuthorizationServiceRequestDelegateTests.swift in Sources */, 9601C371256DD25900C084F5 /* OktaOidcBrowserTaskMacMock.swift in Sources */, 2F32CC3C229D4D11003A6768 /* OktaOidcApiMock.swift in Sources */, + 92DB056B2751129E00B3714F /* OktaOidcErrorTests.swift in Sources */, A17E3A162358FA3300837873 /* OKTRPProfileCode.m in Sources */, A17E3A102358FA3200837873 /* OKTAuthorizationResponseTests.m in Sources */, A17E3A122358FA3300837873 /* OKTURLQueryComponentTestsIOS7.m in Sources */,