Skip to content

Commit

Permalink
Merge pull request #3 from okta/mike.nachbaur_APIUpdates
Browse files Browse the repository at this point in the history
Add support for messages, and improve test coverage for API response parsing
  • Loading branch information
mikenachbaur-okta authored Jan 19, 2021
2 parents 324c771 + c4664c9 commit 0e36467
Show file tree
Hide file tree
Showing 17 changed files with 458 additions and 70 deletions.
3 changes: 3 additions & 0 deletions Sources/IDXClientAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ public protocol IDXClientAPI {
/// - error: Describes the error that occurred, or `nil` if successful.
func start(completion: @escaping (_ response: IDXClient.Response?, _ error: Error?) -> Void)

/// Indicates whether or not the current stage in the workflow can be cancelled.
var canCancel: Bool { get }

/// Cancels the current workflow.
/// - Parameters:
/// - completion: Invoked when the operation is cancelled.
Expand Down
15 changes: 15 additions & 0 deletions Sources/Internal/Extensions/IDXClient+Combine.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,5 +65,20 @@ extension IDXClient.Response {
}
}
}

/// Requests to cancel a remediation step, returning a Future.
public func cancel() -> Future<IDXClient.Response, Error> {
return Future<IDXClient.Response, Error> { (promise) in
self.cancel() { (response, error) in
if let error = error {
promise(.failure(error))
} else if let response = response {
promise(.success(response))
} else {
promise(.failure(IDXClientError.invalidResponseData))
}
}
}
}
}
#endif
4 changes: 4 additions & 0 deletions Sources/Internal/Extensions/IDXClient+Extension.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ extension IDXClient {
}
}

public var canCancel: Bool {
return self.api.canCancel
}

public func cancel(completion: @escaping (Response?, Error?) -> Void)
{
self.api.cancel { (response, error) in
Expand Down
114 changes: 77 additions & 37 deletions Sources/Internal/Extensions/IDXClient+Response.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,19 @@ public extension IDXClient {
/// An object describing the sort of remediation steps available to the user, or `nil` if the workflow is ended.
public let remediation: Remediation?

/// The list of messages sent from the server, or `nil` if no messages are available at the response level.
///
/// Messages reported from the server are usually errors, but may include other information relevant to the user. They should be displayed to the user in the context of the remediation form itself.
public let messages: [Message]?

/// Indicates whether or not the user has logged in successfully. If this is `true`, this response object should be exchanged for access tokens utilizing the `exchangeCode` method.
public var isLoginSuccessful: Bool {
return successResponse != nil
}

/// Indicates whether or not the response can be cancelled.
public let canCancel: Bool

/// Cancels the current workflow.
/// - Parameters:
/// - completion: Invoked when the operation is cancelled.
Expand Down Expand Up @@ -64,7 +72,7 @@ public extension IDXClient {

internal let cancelRemediationOption: Remediation.Option?
internal let successResponse: Remediation.Option?
internal init(client: IDXClientAPIImpl, stateHandle: String, version: String, expiresAt: Date, intent: String, remediation: Remediation?, cancel: Remediation.Option?, success: Remediation.Option?) {
internal init(client: IDXClientAPIImpl, stateHandle: String, version: String, expiresAt: Date, intent: String, remediation: Remediation?, cancel: Remediation.Option?, success: Remediation.Option?, messages: [Message]?) {
self.client = client
self.stateHandle = stateHandle
self.version = version
Expand All @@ -73,6 +81,8 @@ public extension IDXClient {
self.remediation = remediation
self.cancelRemediationOption = cancel
self.successResponse = success
self.messages = messages
self.canCancel = (cancel != nil)

super.init()
}
Expand Down Expand Up @@ -169,10 +179,31 @@ public extension IDXClient {
/// For form fields that have specific options the user can choose from (e.g. security question, passcode, etc), this indicates the different form options that should be displayed to the user.
public let options: [FormValue]?

/// The list of messages sent from the server, or `nil` if no messages are available at the form value level.
///
/// Messages reported from the server at the FormValue level should be considered relevant to the individual form field, and as a result should be displayed to the user alongside any UI elements associated with it.
public let messages: [Message]?

public func relatesTo() -> AnyObject? {
return nil
}

/// For composite or nested forms, this method composes the list of form values, merging the supplied parameters along with the defaults included in the form.
///
/// Validation checks for required and immutable values are performed, which will throw exceptions if any of those parameters fail validation.
/// - Parameter params: User-supplied parameters, `nil` to simply retrieve the defaults.
/// - Throws:
/// - IDXClientError.invalidParameter
/// - IDXClientError.parameterImmutable
/// - IDXClientError.missingRequiredParameter
/// - Returns: Collection of key/value pairs, or `nil` if this form value does not contain a nested form.
/// - SeeAlso: IDXClient.Remediation.Option.formValues(with:)
public func formValues(with params: [String:Any]? = nil) throws -> [String:Any]? {
guard let form = form else { return nil }

return try IDXClient.extractFormValues(from: form, with: params)
}

internal init(name: String?,
label: String?,
type: String?,
Expand All @@ -182,7 +213,8 @@ public extension IDXClient {
required: Bool,
secret: Bool,
form: [FormValue]?,
options: [FormValue]?)
options: [FormValue]?,
messages: [Message]?)
{
self.name = name
self.label = label
Expand All @@ -194,11 +226,12 @@ public extension IDXClient {
self.secret = secret
self.form = form
self.options = options
self.messages = messages

super.init()
}
}

/// Instances of `IDXClient.Remediation.Option` describe choices the user can make to proceed through the authentication workflow.
///
/// Either simple or complex authentication scenarios consist of a set of steps that may be followed, but at some times the user may have a choice in what they use to verify their identity. For example, a user may have multiple choices in verifying their account, such as:
Expand Down Expand Up @@ -259,44 +292,51 @@ public extension IDXClient {
}

/// Apply the remediation option parameters, reconciling default values and mutability requirements.
/// - Parameter params: Optional parameters supplied by the user.
/// - Throws::
///
/// Validation checks for required and immutable values are performed, which will throw exceptions if any of those parameters fail validation.
/// - Parameter params: User-supplied parameters, `nil` to simply retrieve the defaults.
/// - Throws:
/// - IDXClientError.invalidParameter
/// - IDXClientError.parameterImmutable
/// - IDXClientError.missingRequiredParameter
/// - Returns: Dictionary of key/value pairs to send to the remediation endpoint
internal func formValues(with params: [String:Any]? = nil) throws -> [String:Any] {
var result: [String:Any] = form
.filter { $0.value != nil && $0.name != nil }
.reduce(into: [:]) { (result, formValue) in
result[formValue.name!] = formValue.value
}

let allFormValues = form.reduce(into: [String:FormValue]()) { (result, value) in
result[value.name] = value
}

try params?.forEach { (key, value) in
guard let formValue = allFormValues[key] else {
throw IDXClientError.invalidParameter(name: key)
}

guard formValue.mutable == true else {
throw IDXClientError.parameterImmutable(name: key)
}

result[key] = value
}

try allFormValues.values.filter { $0.required }.forEach {
/// TODO: Fix compound field support and relatesTo
guard result[$0.name!] != nil else {
throw IDXClientError.missingRequiredParameter(name: $0.name!)
}
}

return result
/// - Returns: Collection of key/value pairs, or `nil` if this form value does not contain a nested form.
/// - SeeAlso: IDXClient.Remediation.FormValue.formValues(with:)
public func formValues(with params: [String:Any]? = nil) throws -> [String:Any] {
return try IDXClient.extractFormValues(from: form, with: params)
}
}
}

/// Represents messages sent from the server to indicate error or warning conditions related to responses or form values.
@objc(IDXMessage)
final class Message: NSObject {
/// Enumeration describing the type of message.
public enum MessageClass: String {
case error = "ERROR"
case info = "INFO"
case unknown
}

/// The type of message received from the server
public let type: MessageClass

/// A localization key representing this message.
///
/// This allows the text represented by this message to be customized or localized as needed.
public let localizationKey: String

/// The default text for this message.
public let message: String

internal init(type: String,
localizationKey: String,
message: String)
{
self.type = MessageClass(rawValue: type) ?? .unknown
self.localizationKey = localizationKey
self.message = message

super.init()
}
}
}
11 changes: 2 additions & 9 deletions Sources/Internal/Extensions/IDXVersion+Extension.swift
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
//
// IdentityEngineVersion+Extension.swift
// IDXVersion+Extension.swift
// okta-idx-ios
//
// Created by Mike Nachbaur on 2020-12-08.
// Created by Mike Nachbaur on 2021-01-11.
//

import Foundation
Expand All @@ -27,11 +27,4 @@ extension IDXClient.Version: RawRepresentable {
return nil
}
}

internal func clientImplementation(with configuration: IDXClient.Configuration) -> IDXClientAPIImpl {
switch self {
case .v1_0_0:
return IDXClient.APIVersion1(with: configuration)
}
}
}
17 changes: 17 additions & 0 deletions Sources/Internal/Extensions/IDXVersion+InternalExtension.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
//
// IdentityEngineVersion+Extension.swift
// okta-idx-ios
//
// Created by Mike Nachbaur on 2020-12-08.
//

import Foundation

extension IDXClient.Version {
internal func clientImplementation(with configuration: IDXClient.Configuration) -> IDXClientAPIImpl {
switch self {
case .v1_0_0:
return IDXClient.APIVersion1(with: configuration)
}
}
}
42 changes: 42 additions & 0 deletions Sources/Internal/Implementations/IDXClientAPIImpl.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,45 @@ internal protocol IDXClientAPIRequest {
using configuration: IDXClient.Configuration,
completion: @escaping (ResponseType?, Error?) -> Void)
}

internal extension IDXClient {
class func extractFormValues(from form: [IDXClient.Remediation.FormValue], with params: [String:Any]? = nil) throws -> [String:Any] {
var result: [String:Any] = try form
.filter { $0.value != nil && $0.name != nil }
.reduce(into: [:]) { (result, formValue) in
guard let name = formValue.name else { throw IDXClientError.invalidParameter(name: "") }
result[name] = formValue.value
}

let allFormValues = form.reduce(into: [String:IDXClient.Remediation.FormValue]()) { (result, value) in
result[value.name] = value
}

try params?.forEach { (key, value) in
guard let formValue = allFormValues[key] else {
throw IDXClientError.invalidParameter(name: key)
}

guard formValue.mutable == true else {
throw IDXClientError.parameterImmutable(name: key)
}


if let nestedForm = value as? IDXClient.Remediation.FormValue {
result[key] = try nestedForm.formValues()
} else {
result[key] = value
}
}

try allFormValues.values.filter { $0.required }.forEach {
/// TODO: Fix compound field support and relatesTo
guard result[$0.name!] != nil else {
throw IDXClientError.missingRequiredParameter(name: $0.name!)
}
}

return result
}

}
4 changes: 4 additions & 0 deletions Sources/Internal/Implementations/v1.0.0/IDXClientAPIv1.swift
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ extension IDXClient.APIVersion1: IDXClientAPIImpl {
}
}

var canCancel: Bool {
return (cancelRemediationOption != nil)
}

func cancel(completion: @escaping (IDXClient.Response?, Error?) -> Void) {
guard let cancelOption = cancelRemediationOption else {
completion(nil, IDXClientError.unknownRemediationOption(name: "cancel"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ extension URLSession: URLSessionProtocol {
return
}

guard httpResponse.statusCode == 200 else {
guard httpResponse.statusCode <= 400 else {
completionHandler(data, httpResponse, IDXClientError.invalidHTTPResponse)
return
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,17 @@ extension IDXClient.Response {
intent: object.intent,
remediation: IDXClient.Remediation(client: client, v1: object.remediation),
cancel: IDXClient.Remediation.Option(client: client, v1: object.cancel),
success: IDXClient.Remediation.Option(client: client, v1: object.successWithInteractionCode))
success: IDXClient.Remediation.Option(client: client, v1: object.successWithInteractionCode),
messages: object.messages?.value.compactMap { IDXClient.Message(client: client, v1: $0) })
}
}

extension IDXClient.Message {
internal convenience init?(client: IDXClientAPIImpl, v1 object: IDXClient.APIVersion1.Response.Message?) {
guard let object = object else { return nil }
self.init(type: object.type,
localizationKey: object.i18n.key,
message: object.message)
}
}

Expand Down Expand Up @@ -56,8 +66,9 @@ extension IDXClient.Remediation.FormValue {
mutable: object.mutable ?? true,
required: object.required ?? false,
secret: object.secret ?? false,
form: nil/*object.form?.map { IDXClient.FormValue(client: client, v1: $0) }*/,
options: nil/*object.options?.map { IDXClient.FormValue(client: client, v1: $0) }*/)
form: object.form?.value.map { IDXClient.Remediation.FormValue(client: client, v1: $0) },
options: object.options?.map { IDXClient.Remediation.FormValue(client: client, v1: $0) },
messages: object.messages?.value.compactMap { IDXClient.Message(client: client, v1: $0) })
}
}

Expand Down
Loading

0 comments on commit 0e36467

Please sign in to comment.