Skip to content

Commit

Permalink
Merge pull request #140 from chkpnt/dataType
Browse files Browse the repository at this point in the history
Optional dataType
  • Loading branch information
BasThomas authored Nov 17, 2022
2 parents b66cd5e + 3b0bd4e commit e5b1e27
Show file tree
Hide file tree
Showing 4 changed files with 230 additions and 62 deletions.
36 changes: 25 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ public final class MockedData {
``` swift
let originalURL = URL(string: "https://www.wetransfer.com/example.json")!

let mock = Mock(url: originalURL, dataType: .json, statusCode: 200, data: [
let mock = Mock(url: originalURL, contentType: .json, statusCode: 200, data: [
.get : try! Data(contentsOf: MockedData.exampleJSON) // Data containing the JSON response
])
mock.register()
Expand All @@ -102,14 +102,28 @@ URLSession.shared.dataTask(with: originalURL) { (data, response, error) in
}.resume()
```

##### Empty Responses
``` swift
let originalURL = URL(string: "https://www.wetransfer.com/api/foobar")!
var request = URLRequest(url: originalURL)
request.httpMethod = "PUT"

let mock = Mock(request: request, statusCode: 204)
mock.register()

URLSession.shared.dataTask(with: originalURL) { (data, response, error) in
// ....
}.resume()
```

##### Ignoring the query
Some URLs like authentication URLs contain timestamps or UUIDs in the query. To mock these you can ignore the Query for a certain URL:

``` swift
/// Would transform to "https://www.example.com/api/authentication" for example.
let originalURL = URL(string: "https://www.example.com/api/authentication?oauth_timestamp=151817037")!

let mock = Mock(url: originalURL, ignoreQuery: true, dataType: .json, statusCode: 200, data: [
let mock = Mock(url: originalURL, ignoreQuery: true, contentType: .json, statusCode: 200, data: [
.get : try! Data(contentsOf: MockedData.exampleJSON) // Data containing the JSON response
])
mock.register()
Expand All @@ -129,7 +143,7 @@ URLSession.shared.dataTask(with: originalURL) { (data, response, error) in
```swift
let imageURL = URL(string: "https://www.wetransfer.com/sample-image.png")!

Mock(fileExtensions: "png", dataType: .imagePNG, statusCode: 200, data: [
Mock(fileExtensions: "png", contentType: .imagePNG, statusCode: 200, data: [
.get: try! Data(contentsOf: MockedData.botAvatarImageFileUrl)
]).register()

Expand All @@ -142,7 +156,7 @@ URLSession.shared.dataTask(with: imageURL) { (data, response, error) in
```swift
let exampleURL = URL(string: "https://www.wetransfer.com/api/endpoint")!

Mock(url: exampleURL, dataType: .json, statusCode: 200, data: [
Mock(url: exampleURL, contentType: .json, statusCode: 200, data: [
.head: try! Data(contentsOf: MockedData.headResponse),
.get: try! Data(contentsOf: MockedData.exampleJSON)
]).register()
Expand All @@ -158,7 +172,7 @@ In addition to the already build in static `DataType` implementations it is poss
```swift
let xmlURL = URL(string: "https://www.wetransfer.com/sample-xml.xml")!

Mock(fileExtensions: "png", dataType: .init(name: "xml", headerValue: "text/xml"), statusCode: 200, data: [
Mock(fileExtensions: "png", contentType: .init(name: "xml", headerValue: "text/xml"), statusCode: 200, data: [
.get: try! Data(contentsOf: MockedData.sampleXML)
]).register()

Expand All @@ -174,7 +188,7 @@ Sometimes you want to test if the cancellation of requests is working. In that c
```swift
let exampleURL = URL(string: "https://www.wetransfer.com/api/endpoint")!

var mock = Mock(url: exampleURL, dataType: .json, statusCode: 200, data: [
var mock = Mock(url: exampleURL, contentType: .json, statusCode: 200, data: [
.head: try! Data(contentsOf: MockedData.headResponse),
.get: try! Data(contentsOf: MockedData.exampleJSON)
])
Expand All @@ -194,8 +208,8 @@ By creating a mock for the short URL and the redirect URL, you can mock redirect

```swift
let urlWhichRedirects: URL = URL(string: "https://we.tl/redirect")!
Mock(url: urlWhichRedirects, dataType: .html, statusCode: 200, data: [.get: try! Data(contentsOf: MockedData.redirectGET)]).register()
Mock(url: URL(string: "https://wetransfer.com/redirect")!, dataType: .json, statusCode: 200, data: [.get: try! Data(contentsOf: MockedData.exampleJSON)]).register()
Mock(url: urlWhichRedirects, contentType: .html, statusCode: 200, data: [.get: try! Data(contentsOf: MockedData.redirectGET)]).register()
Mock(url: URL(string: "https://wetransfer.com/redirect")!, contentType: .json, statusCode: 200, data: [.get: try! Data(contentsOf: MockedData.exampleJSON)]).register()
```

##### Ignoring URLs
Expand Down Expand Up @@ -223,7 +237,7 @@ Mocker.mode = .optout
You can request a `Mock` to return an error, allowing testing of error handling.

```swift
Mock(url: originalURL, dataType: .json, statusCode: 500, data: [.get: Data()],
Mock(url: originalURL, contentType: .json, statusCode: 500, data: [.get: Data()],
requestError: TestExampleError.example).register()

URLSession.shared.dataTask(with: originalURL) { (data, urlresponse, err) in
Expand All @@ -245,7 +259,7 @@ URLSession.shared.dataTask(with: originalURL) { (data, urlresponse, err) in
You can register on `Mock` callbacks to make testing easier.

```swift
var mock = Mock(url: request.url!, dataType: .json, statusCode: 200, data: [.post: Data()])
var mock = Mock(url: request.url!, contentType: .json, statusCode: 200, data: [.post: Data()])
mock.onRequestHandler = OnRequestHandler(httpBodyType: [[String:String]].self, callback: { request, postBodyArguments in
XCTAssertEqual(request.url, mock.request.url)
XCTAssertEqual(expectedParameters, postBodyArguments)
Expand All @@ -261,7 +275,7 @@ mock.register()
Instead of setting the `completion` and `onRequest` you can also make use of expectations:

```swift
var mock = Mock(url: url, dataType: .json, statusCode: 200, data: [.get: Data()])
var mock = Mock(url: url, contentType: .json, statusCode: 200, data: [.get: Data()])
let requestExpectation = expectationForRequestingMock(&mock)
let completionExpectation = expectationForCompletingMock(&mock)
mock.register()
Expand Down
158 changes: 144 additions & 14 deletions Sources/Mocker/Mock.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,14 @@ public struct Mock: Equatable {

public typealias OnRequest = (_ request: URLRequest, _ httpBodyArguments: [String: Any]?) -> Void

/// The type of the data which is returned.
public let dataType: DataType
/// The type of the data which designates the Content-Type header.
@available(*, deprecated, message: "Calling this property is unsafe after migrating to the `contentType` initializers, and will be removed in an upcoming release. Use `contentType` instead.")
public var dataType: DataType {
return contentType!
}

/// The type of the data which designates the Content-Type header. If set to `nil`, no Content-Type header is added to the headers.
public let contentType: DataType?

/// If set, the error that URLProtocol will report as a result rather than returning data from the mock
public let requestError: Error?
Expand Down Expand Up @@ -101,22 +107,28 @@ public struct Mock: Equatable {
/// Can only be set internally as it's used by the `expectationForCompletingMock(_:)` method.
var onCompletedExpectation: XCTestExpectation?

private init(url: URL? = nil, ignoreQuery: Bool = false, cacheStoragePolicy: URLCache.StoragePolicy = .notAllowed, dataType: DataType, statusCode: Int, data: [HTTPMethod: Data], requestError: Error? = nil, additionalHeaders: [String: String] = [:], fileExtensions: [String]? = nil) {
private init(url: URL? = nil, ignoreQuery: Bool = false, cacheStoragePolicy: URLCache.StoragePolicy = .notAllowed, contentType: DataType? = nil, statusCode: Int, data: [HTTPMethod: Data], requestError: Error? = nil, additionalHeaders: [String: String] = [:], fileExtensions: [String]? = nil) {
guard data.count > 0 else {
preconditionFailure("At least one entry is required in the data dictionary")
}

self.urlToMock = url
let generatedURL = URL(string: "https://mocked.wetransfer.com/\(dataType.name)/\(statusCode)/\(data.keys.first!.rawValue)")!
let generatedURL = URL(string: "https://mocked.wetransfer.com/\(contentType?.name ?? "no-content")/\(statusCode)/\(data.keys.first!.rawValue)")!
self.generatedURL = generatedURL
var request = URLRequest(url: url ?? generatedURL)
request.httpMethod = data.keys.first!.rawValue
self.request = request
self.ignoreQuery = ignoreQuery
self.requestError = requestError
self.dataType = dataType
self.contentType = contentType
self.statusCode = statusCode
self.data = data
self.cacheStoragePolicy = cacheStoragePolicy

var headers = additionalHeaders
headers["Content-Type"] = dataType.headerValue
if let contentType = contentType {
headers["Content-Type"] = contentType.headerValue
}
self.headers = headers

self.fileExtensions = fileExtensions?.map({ $0.replacingOccurrences(of: ".", with: "") })
Expand All @@ -125,12 +137,38 @@ public struct Mock: Equatable {
/// Creates a `Mock` for the given data type. The mock will be automatically matched based on a URL created from the given parameters.
///
/// - Parameters:
/// - dataType: The type of the data which is returned.
/// - dataType: The type of the data which designates the Content-Type header.
/// - statusCode: The HTTP status code to return with the response.
/// - data: The data which will be returned as the response based on the HTTP Method.
/// - additionalHeaders: Additional headers to be added to the response.
@available(*, deprecated, renamed: "init(contentType:statusCode:data:additionalHeaders:)")
public init(dataType: DataType, statusCode: Int, data: [HTTPMethod: Data], additionalHeaders: [String: String] = [:]) {
self.init(url: nil, dataType: dataType, statusCode: statusCode, data: data, additionalHeaders: additionalHeaders, fileExtensions: nil)
self.init(
url: nil,
contentType: dataType,
statusCode: statusCode,
data: data,
additionalHeaders: additionalHeaders,
fileExtensions: nil
)
}

/// Creates a `Mock` for the given content type. The mock will be automatically matched based on a URL created from the given parameters.
///
/// - Parameters:
/// - contentType: The type of the data which designates the Content-Type header. Defaults to `nil`, which means that no Content-Type header is added to the headers.
/// - statusCode: The HTTP status code to return with the response.
/// - data: The data which will be returned as the response based on the HTTP Method.
/// - additionalHeaders: Additional headers to be added to the response.
public init(contentType: DataType?, statusCode: Int, data: [HTTPMethod: Data], additionalHeaders: [String: String] = [:]) {
self.init(
url: nil,
contentType: contentType,
statusCode: statusCode,
data: data,
additionalHeaders: additionalHeaders,
fileExtensions: nil
)
}

/// Creates a `Mock` for the given URL.
Expand All @@ -139,25 +177,117 @@ public struct Mock: Equatable {
/// - url: The URL 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`.
/// - reportFailure: if `true`, the URLsession will report an error loading the URL rather than returning data. Defaults to `false`.
/// - dataType: The type of the data which is returned.
/// - dataType: The type of the data which designates the Content-Type header.
/// - statusCode: The HTTP status code to return with the response.
/// - data: The data which will be returned as the response based on the HTTP Method.
/// - additionalHeaders: Additional headers to be added to the response.
/// - requestError: If provided, the URLSession will report the passed error rather than returning data. Defaults to `nil`.
@available(*, deprecated, renamed: "init(url:ignoreQuery:cacheStoragePolicy:contentType:statusCode:data:additionalHeaders:requestError:)")
public init(url: URL, ignoreQuery: Bool = false, cacheStoragePolicy: URLCache.StoragePolicy = .notAllowed, dataType: DataType, statusCode: Int, data: [HTTPMethod: Data], additionalHeaders: [String: String] = [:], requestError: Error? = nil) {
self.init(url: url, ignoreQuery: ignoreQuery, cacheStoragePolicy: cacheStoragePolicy, dataType: dataType, statusCode: statusCode, data: data, requestError: requestError, additionalHeaders: additionalHeaders, fileExtensions: nil)
self.init(
url: url,
ignoreQuery: ignoreQuery,
cacheStoragePolicy: cacheStoragePolicy,
contentType: dataType,
statusCode: statusCode,
data: data,
requestError: requestError,
additionalHeaders: additionalHeaders,
fileExtensions: nil
)
}


/// Creates a `Mock` for the given URL.
///
/// - Parameters:
/// - url: The URL 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`.
/// - contentType: The type of the data which designates the Content-Type header. Defaults to `nil`, which means that no Content-Type header is added to the headers.
/// - statusCode: The HTTP status code to return with the response.
/// - data: The data which will be returned as the response based on the HTTP Method.
/// - additionalHeaders: Additional headers to be added to the response.
/// - requestError: If provided, the URLSession will report the passed error rather than returning data. Defaults to `nil`.
public init(url: URL, ignoreQuery: Bool = false, cacheStoragePolicy: URLCache.StoragePolicy = .notAllowed, contentType: DataType? = nil, statusCode: Int, data: [HTTPMethod: Data], additionalHeaders: [String: String] = [:], requestError: Error? = nil) {
self.init(
url: url,
ignoreQuery: ignoreQuery,
cacheStoragePolicy: cacheStoragePolicy,
contentType: contentType,
statusCode: statusCode,
data: data,
requestError: requestError,
additionalHeaders: additionalHeaders,
fileExtensions: nil
)
}

/// Creates a `Mock` for the given file extensions. The mock will only be used for urls matching the extension.
///
/// - Parameters:
/// - fileExtensions: The file extension to match for.
/// - dataType: The type of the data which is returned.
/// - dataType: The type of the data which designates the Content-Type header.
/// - statusCode: The HTTP status code to return with the response.
/// - data: The data which will be returned as the response based on the HTTP Method.
/// - additionalHeaders: Additional headers to be added to the response.
@available(*, deprecated, renamed: "init(fileExtensions:contentType:statusCode:data:additionalHeaders:)")
public init(fileExtensions: String..., dataType: DataType, statusCode: Int, data: [HTTPMethod: Data], additionalHeaders: [String: String] = [:]) {
self.init(url: nil, dataType: dataType, statusCode: statusCode, data: data, additionalHeaders: additionalHeaders, fileExtensions: fileExtensions)
self.init(
url: nil,
contentType: dataType,
statusCode: statusCode,
data: data,
additionalHeaders: additionalHeaders,
fileExtensions: fileExtensions
)
}

/// Creates a `Mock` for the given file extensions. The mock will only be used for urls matching the extension.
///
/// - Parameters:
/// - fileExtensions: The file extension to match for.
/// - contentType: The type of the data which designates the Content-Type header. Defaults to `nil`, which means that no Content-Type header is added to the headers.
/// - statusCode: The HTTP status code to return with the response.
/// - data: The data which will be returned as the response based on the HTTP Method.
/// - additionalHeaders: Additional headers to be added to the response.
public init(fileExtensions: String..., contentType: DataType? = nil, statusCode: Int, data: [HTTPMethod: Data], additionalHeaders: [String: String] = [:]) {
self.init(
url: nil,
contentType: contentType,
statusCode: statusCode,
data: data,
additionalHeaders: additionalHeaders,
fileExtensions: fileExtensions
)
}

/// 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`.
/// - contentType: The type of the data which designates the Content-Type header. Defaults to `nil`, which means that no Content-Type header is added to the headers.
/// - statusCode: The HTTP status code to return with the response.
/// - data: The data which will be returned as the response. Defaults to an empty `Data` instance.
/// - additionalHeaders: Additional headers to be added to the response.
/// - requestError: If provided, the URLSession will report the passed error rather than returning data. Defaults to `nil`.
public init(request: URLRequest, ignoreQuery: Bool = false, cacheStoragePolicy: URLCache.StoragePolicy = .notAllowed, contentType: DataType? = nil, statusCode: Int, data: Data = Data(), additionalHeaders: [String: String] = [:], requestError: Error? = nil) {
guard let requestHTTPMethod = Mock.HTTPMethod(rawValue: request.httpMethod ?? "") else {
preconditionFailure("Unexpected http method")
}

self.init(
url: request.url,
ignoreQuery: ignoreQuery,
cacheStoragePolicy: cacheStoragePolicy,
contentType: contentType,
statusCode: statusCode,
data: [requestHTTPMethod: data],
requestError: requestError,
additionalHeaders: additionalHeaders,
fileExtensions: nil
)
}

/// Registers the mock with the shared `Mocker`.
Expand Down
8 changes: 4 additions & 4 deletions Tests/MockerTests/MockTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@ final class MockTests: XCTestCase {

/// It should match two file extension mocks correctly.
func testFileExtensionMocksComparing() {
let mock200 = Mock(fileExtensions: "png", dataType: .imagePNG, statusCode: 200, data: [.put: Data()])
let secondMock200 = Mock(fileExtensions: "png", dataType: .imagePNG, statusCode: 200, data: [.put: Data()])
let mock400 = Mock(fileExtensions: "png", dataType: .imagePNG, statusCode: 400, data: [.put: Data()])
let mockJPEG = Mock(fileExtensions: "jpeg", dataType: .imagePNG, statusCode: 200, data: [.put: Data()])
let mock200 = Mock(fileExtensions: "png", contentType: .imagePNG, statusCode: 200, data: [.put: Data()])
let secondMock200 = Mock(fileExtensions: "png", contentType: .imagePNG, statusCode: 200, data: [.put: Data()])
let mock400 = Mock(fileExtensions: "png", contentType: .imagePNG, statusCode: 400, data: [.put: Data()])
let mockJPEG = Mock(fileExtensions: "jpeg", contentType: .imagePNG, statusCode: 200, data: [.put: Data()])

XCTAssertEqual(mock200, secondMock200)
XCTAssertEqual(mock200, mock400)
Expand Down
Loading

0 comments on commit e5b1e27

Please sign in to comment.