Skip to content

Commit

Permalink
POC response handler for dynamic requests
Browse files Browse the repository at this point in the history
  • Loading branch information
teameh committed Oct 4, 2024
1 parent 4cdf3d8 commit 2dd9e01
Show file tree
Hide file tree
Showing 5 changed files with 169 additions and 6 deletions.
27 changes: 27 additions & 0 deletions Sources/Mocker/Mock.swift
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,9 @@ public struct Mock: Equatable {
/// The on request handler which will be executed everytime this `Mock` was started. Can be used within unit tests for validating that a request has been started. The handler must be set before calling `register`.
public var onRequestHandler: OnRequestHandler?

/// Optional response handler which could be used to dynamically generate the response
public var responseHandler: ResponseHandler?

/// Can only be set internally as it's used by the `expectationForRequestingMock(_:)` method.
var onRequestExpectation: XCTestExpectation?

Expand Down Expand Up @@ -290,6 +293,30 @@ public struct Mock: Equatable {
)
}

/// Creates a `Mock` for the given `URLRequest`.
///
/// - Parameters:
/// - request: The URLRequest, from which the URL and request method is used to match for and to return the mocked data for.
/// - ignoreQuery: If `true`, checking the URL will ignore the query and match only for the scheme, host and path. Defaults to `false`.
/// - cacheStoragePolicy: The caching strategy. Defaults to `notAllowed`.
/// - responseHandler: The response handler to dynamicly generate the response for this Mock
public init(request: URLRequest, ignoreQuery: Bool = false, cacheStoragePolicy: URLCache.StoragePolicy = .notAllowed, responseHandler: ResponseHandler) {
guard let requestHTTPMethod = Mock.HTTPMethod(rawValue: request.httpMethod ?? "") else {
preconditionFailure("Unexpected http method")
}

self.init(
url: request.url,
ignoreQuery: ignoreQuery,
cacheStoragePolicy: cacheStoragePolicy,
statusCode: 999, // unused, see responseHandler
data: [requestHTTPMethod: "responseHandler should have been used instead of this".data(using: .utf8)!],
fileExtensions: nil
)

self.responseHandler = responseHandler
}

/// Registers the mock with the shared `Mocker`.
public func register() {
Mocker.register(self)
Expand Down
23 changes: 18 additions & 5 deletions Sources/Mocker/MockingURLProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,16 +36,29 @@ open class MockingURLProtocol: URLProtocol {

/// Returns Mocked data based on the mocks register in the `Mocker`. Will end up in an error when no Mock data is found for the request.
override public func startLoading() {
guard
let mock = Mocker.mock(for: request),
let response = HTTPURLResponse(url: mock.request.url!, statusCode: mock.statusCode, httpVersion: Mocker.httpVersion.rawValue, headerFields: mock.headers),
let data = mock.data(for: request)
else {
guard let mock = Mocker.mock(for: request) else {
print("\n\n 🚨 No mocked data found for url \(String(describing: request.url?.absoluteString)) method \(String(describing: request.httpMethod)). Did you forget to use `register()`? 🚨 \n\n")
client?.urlProtocol(self, didFailWithError: Error.missingMockedData(url: String(describing: request.url?.absoluteString)))
return
}

let response: HTTPURLResponse
let data: Data
if let responseHandler = mock.responseHandler {
(response, data) = responseHandler.handleRequest(request)
} else if let httpResponse = HTTPURLResponse(
url: mock.request.url!,
statusCode: mock.statusCode,
httpVersion: Mocker.httpVersion.rawValue,
headerFields: mock.headers
), let mockData = mock.data(for: request) {
response = httpResponse
data = mockData
} else {
print("\n\n 🚨 Unable to create HTTPURLResponse for mock for url \(String(describing: request.url?.absoluteString)) method \(String(describing: request.httpMethod)). 🚨 \n\n")
return
}

if let onRequestHandler = mock.onRequestHandler {
onRequestHandler.handleRequest(request)
}
Expand Down
3 changes: 2 additions & 1 deletion Sources/Mocker/OnRequestHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ public struct OnRequestHandler {
}
}

private extension URLRequest {
extension URLRequest {
/// We need to use the http body stream data as the URLRequest once launched converts the `httpBody` to this stream of data.
func httpBodyStreamData() -> Data? {
guard let bodyStream = self.httpBodyStream else { return nil }
Expand All @@ -136,3 +136,4 @@ private extension URLRequest {
return data
}
}

90 changes: 90 additions & 0 deletions Sources/Mocker/ResponseHandler.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
//
// RequestResponseHandler.swift
//
//
// Created by Tieme on 03/10/2024.
// Copyright © 2022 WeTransfer. All rights reserved.

import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif

/// A handler for a dynamic response
public struct ResponseHandler {

public typealias OnRequest<HTTPBody> = (_ request: URLRequest, _ httpBody: HTTPBody?) -> (HTTPURLResponse, Data)

private let internalCallback: (_ request: URLRequest) -> (HTTPURLResponse, Data)

/// Creates a new request handler using the given `HTTPBody` type, which can be any `Decodable`.
/// - Parameters:
/// - httpBodyType: The decodable type to use for parsing the request body.
/// - callback: The callback which will be called just before the request executes.
public init<HTTPBody: Decodable>(httpBodyType: HTTPBody.Type?, callback: @escaping OnRequest<HTTPBody>) {
self.init(httpBodyType: httpBodyType, jsonDecoder: JSONDecoder(), callback: callback)
}

/// Creates a new request handler using the given `HTTPBody` type, which can be any `Decodable` and decoding it using the provided `JSONDecoder`.
/// - Parameters:
/// - httpBodyType: The decodable type to use for parsing the request body.
/// - jsonDecoder: The decoder to use for decoding the request body.
/// - callback: The callback which will be called just before the request executes.
public init<HTTPBody: Decodable>(httpBodyType: HTTPBody.Type?, jsonDecoder: JSONDecoder, callback: @escaping OnRequest<HTTPBody>) {
self.internalCallback = { request in
guard
let httpBody = request.httpBodyStreamData() ?? request.httpBody,
let decodedObject = try? jsonDecoder.decode(HTTPBody.self, from: httpBody)
else {
return callback(request, nil)
}
return callback(request, decodedObject)
}
}

/// Creates a new request handler using the given callback to call on request without parsing the body arguments.
/// - Parameter requestCallback: The callback which will be executed just before the request executes, containing the request.
public init(requestCallback: @escaping (_ request: URLRequest) -> (HTTPURLResponse, Data)) {
self.internalCallback = requestCallback
}

/// Creates a new request handler using the given callback to call on request without parsing the body arguments and without passing the request.
/// - Parameter callback: The callback which will be executed just before the request executes.
public init(callback: @escaping () -> (HTTPURLResponse, Data)) {
self.internalCallback = { _ in
callback()
}
}

/// Creates a new request handler using the given callback to call on request.
/// - Parameter jsonDictionaryCallback: The callback that executes just before the request executes, containing the HTTP Body Arguments as a JSON Object Dictionary.
public init(jsonDictionaryCallback: @escaping ((_ request: URLRequest, _ httpBodyArguments: [String: Any]?) -> (HTTPURLResponse, Data))) {
self.internalCallback = { request in
guard
let httpBody = request.httpBodyStreamData() ?? request.httpBody,
let jsonObject = try? JSONSerialization.jsonObject(with: httpBody, options: .fragmentsAllowed) as? [String: Any]
else {
return jsonDictionaryCallback(request, nil)
}
return jsonDictionaryCallback(request, jsonObject)
}
}

/// Creates a new request handler using the given callback to call on request.
/// - Parameter jsonDictionaryCallback: The callback that executes just before the request executes, containing the HTTP Body Arguments as a JSON Object Array.
public init(jsonArrayCallback: @escaping ((_ request: URLRequest, _ httpBodyArguments: [[String: Any]]?) -> (HTTPURLResponse, Data))) {
self.internalCallback = { request in
guard
let httpBody = request.httpBodyStreamData() ?? request.httpBody,
let jsonObject = try? JSONSerialization.jsonObject(with: httpBody, options: .fragmentsAllowed) as? [[String: Any]]
else {
return jsonArrayCallback(request, nil)
}
return jsonArrayCallback(request, jsonObject)
}
}

func handleRequest(_ request: URLRequest) -> (HTTPURLResponse, Data) {
return internalCallback(request)
}
}
32 changes: 32 additions & 0 deletions Tests/MockerTests/MockerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,38 @@ final class MockerTests: XCTestCase {
wait(for: [onRequestExpectation], timeout: 2.0)
}

func testResponseHandler() {
let requestExpectation = self.expectation(description: "Data request should succeed")
let responseHandlerExpectation = self.expectation(description: "Data request should start")
let originalURL = URL(string: "https://avatars3.githubusercontent.com/u/26250426?v=4&s=400")!
var request = URLRequest(url: originalURL)

let mockedData = MockedData.botAvatarImageFileUrl.data
let mock = Mock(request: request, responseHandler: ResponseHandler(callback: {
responseHandlerExpectation.fulfill()
return (
HTTPURLResponse(
url: originalURL,
statusCode: 200,
httpVersion: Mocker.httpVersion.rawValue,
headerFields: nil
)!,
mockedData
)
}))
mock.register()

mock.register()
URLSession.shared.dataTask(with: originalURL) { (data, _, error) in
XCTAssertNil(error)
XCTAssertEqual(data, mockedData, "Image should be returned mocked")
requestExpectation.fulfill()
}.resume()


wait(for: [responseHandlerExpectation, requestExpectation], enforceOrder: true)
}

/// It should call the mock after a delay.
func testDelayedMock() {
let nonDelayExpectation = expectation(description: "Data request should succeed")
Expand Down

0 comments on commit 2dd9e01

Please sign in to comment.