Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Response handler for dynamic requests (POC) #160

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 36 additions & 3 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 All @@ -312,11 +339,17 @@ public struct Mock: Equatable {
// If the mock contains a file extension, this should always be used to match for.
guard let pathExtension = request.url?.pathExtension else { return false }
return fileExtensions.contains(pathExtension)
} else if mock.ignoreQuery {
return mock.request.url!.baseString == request.url?.baseString && mock.data.keys.contains(requestHTTPMethod)
}

return mock.request.url!.absoluteString == request.url?.absoluteString && mock.data.keys.contains(requestHTTPMethod)
if mock.ignoreQuery {
if mock.request.url!.baseString != request.url?.baseString {
return false
}
} else if mock.request.url!.absoluteString != request.url?.absoluteString {
return false
}

return mock.responseHandler != nil || mock.data.keys.contains(requestHTTPMethod)
}

public static func == (lhs: Mock, rhs: Mock) -> Bool {
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