diff --git a/Samples/EmbeddedAuthWithSDKs/EmbeddedAuth/Base.lproj/Main.storyboard b/Samples/EmbeddedAuthWithSDKs/EmbeddedAuth/Base.lproj/Main.storyboard
index de2c5237..24bfe696 100644
--- a/Samples/EmbeddedAuthWithSDKs/EmbeddedAuth/Base.lproj/Main.storyboard
+++ b/Samples/EmbeddedAuthWithSDKs/EmbeddedAuth/Base.lproj/Main.storyboard
@@ -35,7 +35,7 @@
-
+
@@ -113,6 +113,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -141,6 +160,7 @@
+
diff --git a/Samples/EmbeddedAuthWithSDKs/EmbeddedAuth/ClientConfiguration.swift b/Samples/EmbeddedAuthWithSDKs/EmbeddedAuth/ClientConfiguration.swift
index 465c49ea..ee0d39d2 100644
--- a/Samples/EmbeddedAuthWithSDKs/EmbeddedAuth/ClientConfiguration.swift
+++ b/Samples/EmbeddedAuthWithSDKs/EmbeddedAuth/ClientConfiguration.swift
@@ -16,15 +16,17 @@ struct ClientConfiguration {
private static let issuerKey = "issuerUrl"
private static let clientIdKey = "clientId"
private static let redirectUriKey = "redirectUrl"
+ private static let recoveryTokenKey = "recoveryToken"
private static let scopesKey = "scopes"
let clientId: String
let issuer: String
let redirectUri: String
let scopes: String
+ let recoveryToken: String?
let shouldSave: Bool
- init(clientId: String, issuer: String, redirectUri: String, scopes: String, shouldSave: Bool) throws {
+ init(clientId: String, issuer: String, redirectUri: String, scopes: String, recoveryToken: String?, shouldSave: Bool) throws {
guard let issuerUrl = URL(string: issuer) else {
throw ConfigurationError.invalidUrl(name: "issuer")
}
@@ -58,6 +60,7 @@ struct ClientConfiguration {
self.clientId = clientId
self.scopes = scopes
self.redirectUri = redirectUri
+ self.recoveryToken = recoveryToken
self.shouldSave = shouldSave
}
@@ -66,13 +69,15 @@ struct ClientConfiguration {
"--issuer", "-i",
"--redirectUri", "-r",
"--scopes", "-s",
- "--clientId", "-c"
+ "--clientId", "-c",
+ "--recoveryToken", "-t"
]
var issuer: String?
var clientId: String?
var scopes: String = "openid profile offline_access"
var redirectUri: String?
+ var recoveryToken: String?
var key: String?
for argument in CommandLine.arguments {
if arguments.contains(argument) {
@@ -93,6 +98,9 @@ struct ClientConfiguration {
case "--scopes", "-s":
scopes = argument
+ case "--recoveryToken", "-t":
+ recoveryToken = argument
+
default: break
}
key = nil
@@ -105,6 +113,7 @@ struct ClientConfiguration {
issuer: issuer!,
redirectUri: redirectUri!,
scopes: scopes,
+ recoveryToken: recoveryToken,
shouldSave: false)
}
@@ -127,6 +136,7 @@ struct ClientConfiguration {
issuer: issuer,
redirectUri: redirectUri,
scopes: scopes,
+ recoveryToken: nil,
shouldSave: false)
}
@@ -145,10 +155,16 @@ struct ClientConfiguration {
return nil
}
+ var recoveryToken = defaults.string(forKey: recoveryTokenKey) ?? environment["RECOVERY_TOKEN"]
+ if recoveryToken?.count == 0 {
+ recoveryToken = nil
+ }
+
return try? ClientConfiguration(clientId: clientId,
issuer: issuer,
redirectUri: redirectUri,
scopes: scopes,
+ recoveryToken: recoveryToken,
shouldSave: false)
}
@@ -163,6 +179,7 @@ struct ClientConfiguration {
defaults.setValue(issuer, forKey: type(of: self).issuerKey)
defaults.setValue(clientId, forKey: type(of: self).clientIdKey)
defaults.setValue(redirectUri, forKey: type(of: self).redirectUriKey)
+ defaults.setValue(recoveryToken, forKey: type(of: self).recoveryTokenKey)
defaults.setValue(scopes, forKey: type(of: self).scopesKey)
defaults.synchronize()
diff --git a/Samples/EmbeddedAuthWithSDKs/EmbeddedAuth/Signin/View Controllers/IDXStartViewController.swift b/Samples/EmbeddedAuthWithSDKs/EmbeddedAuth/Signin/View Controllers/IDXStartViewController.swift
index b8ce6f42..1b5b0b95 100644
--- a/Samples/EmbeddedAuthWithSDKs/EmbeddedAuth/Signin/View Controllers/IDXStartViewController.swift
+++ b/Samples/EmbeddedAuthWithSDKs/EmbeddedAuth/Signin/View Controllers/IDXStartViewController.swift
@@ -27,27 +27,31 @@ class IDXStartViewController: UIViewController, IDXSigninController {
return
}
- IDXClient.start(with: signin.configuration) { (client, error) in
- guard let client = client else {
- if let error = error {
- self.showError(error)
-
- signin.failure(with: error)
- }
- return
- }
-
- self.signin?.idx = client
- client.resume { (response, error) in
- guard let response = response else {
- if let error = error {
- self.showError(error)
-
- signin.failure(with: error)
+ var options = [IDXClient.Option:String]()
+ if let recoveryToken = ClientConfiguration.active?.recoveryToken {
+ options[.recoveryToken] = recoveryToken
+ }
+
+ IDXClient.start(with: signin.configuration, options: options) { result in
+ switch result {
+ case .success(let client):
+ self.signin?.idx = client
+ client.resume { (response, error) in
+ guard let response = response else {
+ if let error = error {
+ self.showError(error)
+
+ signin.failure(with: error)
+ }
+ return
}
- return
+ signin.proceed(to: response)
}
- signin.proceed(to: response)
+
+ case .failure(let error):
+ self.showError(error)
+
+ signin.failure(with: error)
}
}
}
diff --git a/Samples/EmbeddedAuthWithSDKs/EmbeddedAuth/View Controllers/ClientConfigurationViewController.swift b/Samples/EmbeddedAuthWithSDKs/EmbeddedAuth/View Controllers/ClientConfigurationViewController.swift
index 619743e8..eaabe86d 100644
--- a/Samples/EmbeddedAuthWithSDKs/EmbeddedAuth/View Controllers/ClientConfigurationViewController.swift
+++ b/Samples/EmbeddedAuthWithSDKs/EmbeddedAuth/View Controllers/ClientConfigurationViewController.swift
@@ -29,6 +29,7 @@ class ClientConfigurationViewController: UIViewController {
@IBOutlet weak var clientIdField: UITextField!
@IBOutlet weak var scopesField: UITextField!
@IBOutlet weak var redirectField: UITextField!
+ @IBOutlet weak var recoveryTokenField: UITextField!
var configuration: ClientConfiguration? = ClientConfiguration.active
override func viewDidLoad() {
@@ -38,12 +39,14 @@ class ClientConfigurationViewController: UIViewController {
clientIdField.text = configuration?.clientId
scopesField.text = configuration?.scopes
redirectField.text = configuration?.redirectUri
-
+ recoveryTokenField.text = configuration?.recoveryToken
+
issuerField.accessibilityIdentifier = "issuerField"
clientIdField.accessibilityIdentifier = "clientIdField"
scopesField.accessibilityIdentifier = "scopesField"
redirectField.accessibilityIdentifier = "redirectField"
-
+ recoveryTokenField.accessibilityIdentifier = "recoveryTokenField"
+
view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(backgroundTapped)))
}
@@ -70,6 +73,7 @@ class ClientConfigurationViewController: UIViewController {
issuer: issuerUrl,
redirectUri: redirectUri,
scopes: scopes,
+ recoveryToken: recoveryTokenField.text,
shouldSave: true)
configuration?.save()
dismiss(animated: true)
@@ -97,8 +101,10 @@ extension ClientConfigurationViewController: UITextFieldDelegate {
case scopesField:
redirectField.becomeFirstResponder()
case redirectField:
- redirectField.resignFirstResponder()
-
+ recoveryTokenField.becomeFirstResponder()
+ case recoveryTokenField:
+ recoveryTokenField.resignFirstResponder()
+
default: break
}
return false
diff --git a/Sources/OktaIdx/IDXClient.swift b/Sources/OktaIdx/IDXClient.swift
index 18d2253c..9c663f7e 100644
--- a/Sources/OktaIdx/IDXClient.swift
+++ b/Sources/OktaIdx/IDXClient.swift
@@ -17,6 +17,14 @@ import Foundation
/// The `IDXClient.Configuration` class is used to communicate which application, defined within Okta, the user is being authenticated with. From this point a workflow is initiated, consisting of a series of authentication "Remediation" steps. At each step, your application can introspect the `Response` object to determine which UI should be presented to your user to guide them through to login.
@objc
public final class IDXClient: NSObject {
+ /// Options to use when initiating an IDXClient.
+ public enum Option: String {
+ /// Option used when a client needs to supply its own custom state value when initiating an IDXClient.
+ case state
+
+ /// Option used when a user is authenticating using a recovery token.
+ case recoveryToken = "recovery_token"
+ }
/// The type used for the completion handler result from any method that returns an `Response`.
/// - Parameters:
@@ -43,26 +51,31 @@ public final class IDXClient: NSObject {
/// Starts a new authentication session using the given configuration values. If the client is able to successfully interact with Okta Identity Engine, a new client instance is returned to the caller.
/// - Parameters:
/// - configuration: Configuration describing the app settings to contact.
- /// - state: Optional state string to use within the OAuth2 transaction.
+ /// - options: Options to include within the OAuth2 transaction.
/// - completion: Completion block to be invoked when a client is created, or when an error is received.
public static func start(with configuration: Configuration,
- state: String? = nil,
+ options: [Option:String]? = nil,
completion: @escaping (Result) -> Void)
{
let api = Version.latest.clientImplementation(with: configuration)
- start(with: api, state: state, completion: completion)
+ start(with: api, options: options, completion: completion)
}
/// Starts a new authentication session using the given configuration values. If the client is able to successfully interact with Okta Identity Engine, a new client instance is returned to the caller.
/// - Parameters:
/// - configuration: Configuration describing the app settings to contact.
- /// - state: Optional state string to use within the OAuth2 transaction.
+ /// - options: Options to include within the OAuth2 transaction.
/// - completion: Completion block to be invoked when a client is created, or when an error is received.
@objc public static func start(with configuration: Configuration,
- state: String? = nil,
+ options: [String:String]? = nil,
completion: @escaping (_ client: IDXClient?, _ error: Error?) -> Void)
{
- start(with: configuration, state: state) { result in
+ let mappedOptions = options?.reduce(into: [Option:String](), { partialResult, item in
+ guard let option = Option(rawValue: item.key) else { return }
+ partialResult[option] = item.value
+ })
+
+ start(with: configuration, options: mappedOptions) { result in
switch result {
case .failure(let error):
completion(nil, error)
@@ -73,10 +86,10 @@ public final class IDXClient: NSObject {
}
static func start(with api: IDXClientAPIImpl,
- state: String? = nil,
+ options: [Option:String]? = nil,
completion: @escaping (Result) -> Void)
{
- api.start(state: state) { result in
+ api.start(options: options) { result in
switch result {
case .failure(let error):
completion(.failure(error))
@@ -206,20 +219,20 @@ extension IDXClient {
/// Starts a new authentication session using the given configuration values. If the client is able to successfully interact with Okta Identity Engine, a new client instance is returned to the caller.
/// - Parameters:
/// - configuration: Configuration describing the app settings to contact.
- /// - state: Optional state string to use within the OAuth2 transaction.
+ /// - options: Options to include within the OAuth2 transaction.
/// - Returns: An IDXClient instance for this session.
public static func start(with configuration: Configuration,
- state: String? = nil) async throws -> IDXClient
+ options: [Option:String]? = nil) async throws -> IDXClient
{
let api = Version.latest.clientImplementation(with: configuration)
- return try await start(with: api, state: state)
+ return try await start(with: api, options: options)
}
static func start(with api: IDXClientAPIImpl,
- state: String? = nil) async throws -> IDXClient
+ options: [Option:String]? = nil) async throws -> IDXClient
{
try await withCheckedThrowingContinuation { continuation in
- start(with: api, state: state) { result in
+ start(with: api, options: options) { result in
continuation.resume(with: result)
}
}
diff --git a/Sources/OktaIdx/Internal/Implementations/IDXClientAPIImpl.swift b/Sources/OktaIdx/Internal/Implementations/IDXClientAPIImpl.swift
index 84a94ce8..97571a3e 100644
--- a/Sources/OktaIdx/Internal/Implementations/IDXClientAPIImpl.swift
+++ b/Sources/OktaIdx/Internal/Implementations/IDXClientAPIImpl.swift
@@ -39,7 +39,7 @@ protocol IDXClientAPIImpl: AnyObject {
/// The upstream client to communicate critical events to
var client: IDXClientAPI? { get set }
- func start(state: String?, completion: @escaping (Result) -> Void)
+ func start(options: [IDXClient.Option:String]?, completion: @escaping (Result) -> Void)
func resume(completion: @escaping (Result) -> Void)
func proceed(remediation option: Remediation,
completion: @escaping (Result) -> Void)
diff --git a/Sources/OktaIdx/Internal/Implementations/Version1/IDXClientAPIv1.swift b/Sources/OktaIdx/Internal/Implementations/Version1/IDXClientAPIv1.swift
index f11a4e74..89a9a955 100644
--- a/Sources/OktaIdx/Internal/Implementations/Version1/IDXClientAPIv1.swift
+++ b/Sources/OktaIdx/Internal/Implementations/Version1/IDXClientAPIv1.swift
@@ -34,22 +34,31 @@ extension IDXClient {
}
extension IDXClient.APIVersion1: IDXClientAPIImpl {
- func start(state: String?, completion: @escaping (Result) -> Void) {
+ func start(options: [IDXClient.Option : String]?, completion: @escaping (Result) -> Void) {
guard let codeVerifier = String.pkceCodeVerifier(),
let codeChallenge = codeVerifier.pkceCodeChallenge() else
{
completion(.failure(.internalMessage("Cannot create a PKCE Code Verifier")))
return
}
+
+ // Ensure we have, at minimum, a state value
+ let state = options?[.state] ?? UUID().uuidString
+ var options = options ?? [:]
+ options[.state] = state
+
+ let mappedOptions = options.reduce(into: [String:String](), { partialResult, item in
+ partialResult[item.key.rawValue] = item.value
+ })
- let request = InteractRequest(state: state, codeChallenge: codeChallenge)
+ let request = InteractRequest(options: mappedOptions, codeChallenge: codeChallenge)
request.send(to: session, using: configuration) { result in
switch result {
case .failure(let error):
completion(.failure(error))
case .success(let response):
completion(.success(IDXClient.Context(configuration: self.configuration,
- state: request.state,
+ state: state,
interactionHandle: response.interactionHandle,
codeVerifier: codeVerifier)))
}
diff --git a/Sources/OktaIdx/Internal/Implementations/Version1/Requests/InteractRequest.swift b/Sources/OktaIdx/Internal/Implementations/Version1/Requests/InteractRequest.swift
index 63c59e25..e0126ab9 100644
--- a/Sources/OktaIdx/Internal/Implementations/Version1/Requests/InteractRequest.swift
+++ b/Sources/OktaIdx/Internal/Implementations/Version1/Requests/InteractRequest.swift
@@ -17,22 +17,15 @@ extension IDXClient.APIVersion1.InteractRequest: IDXClientAPIRequest {
typealias ResponseType = Response
- init(state: String?, codeChallenge: String) {
- self.state = state ?? UUID().uuidString
- self.codeChallenge = codeChallenge
- }
-
func urlRequest(using configuration:IDXClient.Configuration) -> URLRequest? {
guard let url = configuration.issuerUrl(with: "v1/interact") else { return nil }
- let params = [
- "client_id": configuration.clientId,
- "scope": configuration.scopes.joined(separator: " "),
- "code_challenge": codeChallenge,
- "code_challenge_method": "S256",
- "redirect_uri": configuration.redirectUri,
- "state": state
- ]
+ var params = options
+ params["client_id"] = configuration.clientId
+ params["scope"] = configuration.scopes.joined(separator: " ")
+ params["code_challenge"] = codeChallenge
+ params["code_challenge_method"] = "S256"
+ params["redirect_uri"] = configuration.redirectUri
var request = URLRequest(url: url)
request.httpMethod = "POST"
diff --git a/Sources/OktaIdx/Internal/Implementations/Version1/Responses/Responses.swift b/Sources/OktaIdx/Internal/Implementations/Version1/Responses/Responses.swift
index 415f397e..44c71081 100644
--- a/Sources/OktaIdx/Internal/Implementations/Version1/Responses/Responses.swift
+++ b/Sources/OktaIdx/Internal/Implementations/Version1/Responses/Responses.swift
@@ -45,7 +45,7 @@ extension IDXClient.APIVersion1 {
}
struct InteractRequest: HasOAuthHTTPHeaders {
- let state: String
+ let options: [String:String]
let codeChallenge: String
}
diff --git a/Tests/OktaIdxTests/IDXClientAPIVersion1Tests.swift b/Tests/OktaIdxTests/IDXClientAPIVersion1Tests.swift
index bc0244df..d0dd0c81 100644
--- a/Tests/OktaIdxTests/IDXClientAPIVersion1Tests.swift
+++ b/Tests/OktaIdxTests/IDXClientAPIVersion1Tests.swift
@@ -41,10 +41,14 @@ class IDXClientAPIVersion1Tests: XCTestCase {
try session.expect("https://foo.oktapreview.com/oauth2/default/v1/interact", fileName: "interact-response")
let completion = expectation(description: "Response")
- api.start(state: nil) { result in
+ api.start(options: nil) { result in
if case let Result.success(context) = result {
XCTAssertNotNil(context)
XCTAssertEqual(context.interactionHandle, "003Q14X7li")
+
+ // Ensure state is a UUID
+ let state = context.state
+ XCTAssertNotNil(UUID(uuidString: state))
} else {
XCTFail("Not successful")
}
@@ -53,13 +57,46 @@ class IDXClientAPIVersion1Tests: XCTestCase {
wait(for: [completion], timeout: 1)
}
+ func testInteractWithOptions() throws {
+ try session.expect("https://foo.oktapreview.com/oauth2/default/v1/interact", fileName: "interact-response")
+
+ let completion = expectation(description: "Response")
+ api.start(options: [.state: "MyState", .recoveryToken: "someRecoveryToken"]) { result in
+ if case let Result.success(context) = result {
+ XCTAssertNotNil(context)
+ XCTAssertEqual(context.state, "MyState")
+ } else {
+ XCTFail("Not successful")
+ }
+ completion.fulfill()
+ }
+ wait(for: [completion], timeout: 1)
+
+ let request = try XCTUnwrap(session.requests.last)
+
+ XCTAssertEqual(request.url?.absoluteString,
+ "https://foo.oktapreview.com/oauth2/default/v1/interact")
+
+ let data = try XCTUnwrap(request.httpBody)
+ let body = String(data: data, encoding: .utf8)?
+ .components(separatedBy: "&")
+ .reduce(into: [String:String](), { partialResult, item in
+ let items = item.components(separatedBy: "=")
+ guard items.count == 2 else { return }
+ partialResult[items[0]] = items[1]
+ })
+
+ XCTAssertEqual(body?["state"], "MyState")
+ XCTAssertEqual(body?["recovery_token"], "someRecoveryToken")
+ }
+
func testInteractFailure() throws {
try session.expect("https://foo.oktapreview.com/v1/interact",
fileName: "interact-error-response",
statusCode: 400)
let completion = expectation(description: "Response")
- api.start(state: nil) { result in
+ api.start(options: nil) { result in
if case let Result.failure(error) = result {
XCTAssertEqual(error, .invalidResponseData)
} else {
diff --git a/Tests/OktaIdxTests/IDXClientRequestTests.swift b/Tests/OktaIdxTests/IDXClientRequestTests.swift
index 370e29ef..bbce9ce4 100644
--- a/Tests/OktaIdxTests/IDXClientRequestTests.swift
+++ b/Tests/OktaIdxTests/IDXClientRequestTests.swift
@@ -25,7 +25,7 @@ class IDXClientRequestTests: XCTestCase {
redirectUri: "redirect:/uri")
func testInteractRequest() throws {
- let request = IDXClient.APIVersion1.InteractRequest(state: nil, codeChallenge: "ABCEasyas123")
+ let request = IDXClient.APIVersion1.InteractRequest(options: ["state": "state"], codeChallenge: "ABCEasyas123")
let urlRequest = request.urlRequest(using: configuration)
XCTAssertNotNil(urlRequest)
@@ -46,14 +46,11 @@ class IDXClientRequestTests: XCTestCase {
XCTAssertEqual(data?["code_challenge"], "ABCEasyas123")
XCTAssertEqual(data?["code_challenge_method"], "S256")
XCTAssertEqual(data?["redirect_uri"], "redirect:/uri")
-
- // Ensure state is a UUID
- let state = data?["state"]
- XCTAssertNotNil(UUID(uuidString: state!!))
+ XCTAssertEqual(data?["state"], "state")
}
func testInteractRequestWithCustomState() throws {
- let request = IDXClient.APIVersion1.InteractRequest(state: "mystate", codeChallenge: "ABCEasyas123")
+ let request = IDXClient.APIVersion1.InteractRequest(options: ["state": "mystate"], codeChallenge: "ABCEasyas123")
let urlRequest = try XCTUnwrap(request.urlRequest(using: configuration))
let data = try XCTUnwrap(urlRequest.httpBody?.urlFormEncoded())
XCTAssertEqual(data["state"], "mystate")
diff --git a/Tests/OktaIdxTests/IDXClientTests.swift b/Tests/OktaIdxTests/IDXClientTests.swift
index fc009376..ca891914 100644
--- a/Tests/OktaIdxTests/IDXClientTests.swift
+++ b/Tests/OktaIdxTests/IDXClientTests.swift
@@ -78,16 +78,16 @@ class IDXClientTests: XCTestCase {
// start()
expect = expectation(description: "start")
- IDXClient.start(with: api, state: "state") { result in
+ IDXClient.start(with: api, options: [.state: "stateString"]) { result in
called = true
expect.fulfill()
}
wait(for: [ expect ], timeout: 1)
XCTAssertTrue(called)
call = api.recordedCalls.last
- XCTAssertEqual(call?.function, "start(state:completion:)")
+ XCTAssertEqual(call?.function, "start(options:completion:)")
XCTAssertEqual(call?.arguments?.count, 1)
- XCTAssertEqual(call?.arguments?["state"] as! String, "state")
+ XCTAssertEqual(call?.arguments?["options"] as! [IDXClient.Option:String], [.state: "stateString"])
api.reset()
// resume()
diff --git a/Tests/OktaIdxTests/UserAgentTests.swift b/Tests/OktaIdxTests/UserAgentTests.swift
index 22c2330e..1a1ab5e3 100644
--- a/Tests/OktaIdxTests/UserAgentTests.swift
+++ b/Tests/OktaIdxTests/UserAgentTests.swift
@@ -38,7 +38,7 @@ class UserAgentTests: XCTestCase {
}
func testInteractRequest() {
- let request = IDXClient.APIVersion1.InteractRequest(state: nil, codeChallenge: "challenge")
+ let request = IDXClient.APIVersion1.InteractRequest(options: [:], codeChallenge: "challenge")
let userAgent = request.httpHeaders["User-Agent"]
XCTAssertNotNil(userAgent)
diff --git a/Tests/TestCommon/Mocks/IDXClientAPIMock.swift b/Tests/TestCommon/Mocks/IDXClientAPIMock.swift
index 090473c2..4a234497 100644
--- a/Tests/TestCommon/Mocks/IDXClientAPIMock.swift
+++ b/Tests/TestCommon/Mocks/IDXClientAPIMock.swift
@@ -100,10 +100,10 @@ class IDXClientAPIv1Mock: MockBase, IDXClientAPIImpl {
self.configuration = configuration
}
- func start(state: String?, completion: @escaping (Result) -> Void) {
+ func start(options: [IDXClient.Option: String]?, completion: @escaping (Result) -> Void) {
recordedCalls.append(RecordedCall(function: #function,
arguments: [
- "state": state as Any
+ "options": options as Any
]))
completion(result(for: #function))
}
diff --git a/Tests/TestCommon/Mocks/URLSessionMock.swift b/Tests/TestCommon/Mocks/URLSessionMock.swift
index 5ea7e325..ee48dec2 100644
--- a/Tests/TestCommon/Mocks/URLSessionMock.swift
+++ b/Tests/TestCommon/Mocks/URLSessionMock.swift
@@ -38,6 +38,8 @@ class URLSessionMock: URLSessionProtocol {
let error: Error?
}
+ private(set) var requests: [URLRequest] = []
+
private var calls: [String: Call] = [:]
func expect(_ url: String, call: Call) {
calls[url] = call
@@ -81,6 +83,7 @@ class URLSessionMock: URLSessionProtocol {
}
func dataTaskWithRequest(with request: URLRequest, completionHandler: @escaping DataTaskResult) -> URLSessionDataTaskProtocol {
+ requests.append(request)
let response = call(for: request.url!.absoluteString)
return URLSessionDataTaskMock(data: response?.data,
response: response?.response,