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

Optional dataType #140

Merged
merged 4 commits into from
Nov 17, 2022
Merged
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
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)
BasThomas marked this conversation as resolved.
Show resolved Hide resolved
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()])
BasThomas marked this conversation as resolved.
Show resolved Hide resolved
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
BasThomas marked this conversation as resolved.
Show resolved Hide resolved
/// 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.
BasThomas marked this conversation as resolved.
Show resolved Hide resolved
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")
BasThomas marked this conversation as resolved.
Show resolved Hide resolved
}

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.
chkpnt marked this conversation as resolved.
Show resolved Hide resolved
///
/// - 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