123 | ```
124 |
125 | 7. [Open a Pull Request](https://help.github.com/articles/using-pull-requests/)
126 | with a clear title and description.
127 |
128 | ### Conventions of commit messages
129 |
130 | Adding features on repo
131 |
132 | ```bash
133 | git commit -m "feat: message about this feature"
134 | ```
135 |
136 | Fixing features on repo
137 |
138 | ```bash
139 | git commit -m "fix: message about this update"
140 | ```
141 |
142 | Removing features on repo
143 |
144 | ```bash
145 | git commit -m "refactor: message about this" -m "BREAKING CHANGE: message about the breaking change"
146 | ```
147 |
148 |
149 | **IMPORTANT**: By submitting a patch, you agree to allow the project owner to
150 | license your work under the same license as that used by the project.
151 |
152 | ### Discussions
153 |
154 | We aim to keep all project discussion inside GitHub issues. This is to make sure valuable discussion is accessible via search. If you have questions about how to use the library, or how the project is running - GitHub issues are the goto tool for this project.
155 |
156 | #### Our expectations on you as a contributor
157 |
158 | We want contributors to provide ideas, keep the ship shipping and to take some of the load from others. It is non-obligatory; we’re here to get things done in an enjoyable way. 🎉
159 |
160 | The fact that you'll have push access will allow you to:
161 |
162 | - Avoid having to fork the project if you want to submit other pull requests as you'll be able to create branches directly on the project.
163 | - Help triage issues, merge pull requests.
164 | - Pick up the project if other maintainers move their focus elsewhere.
165 |
--------------------------------------------------------------------------------
/Changelog.md:
--------------------------------------------------------------------------------
1 | ### 3.0.2
2 | - Update CI submodule ([#151](https://github.com/WeTransfer/Mocker/pull/151)) via [@AvdLee](https://github.com/AvdLee)
3 | - Simplify tests and add Sendable to subtypes ([#150](https://github.com/WeTransfer/Mocker/pull/150)) via [@AvdLee](https://github.com/AvdLee)
4 | - Sort HTTP methods to remove randomness from keys ([#149](https://github.com/WeTransfer/Mocker/pull/149)) via [@Chewie69006](https://github.com/Chewie69006)
5 | - Optional dataType ([#140](https://github.com/WeTransfer/Mocker/pull/140)) via [@chkpnt](https://github.com/chkpnt)
6 | - Merge release 3.0.1 into master ([#141](https://github.com/WeTransfer/Mocker/pull/141)) via [@wetransferplatform](https://github.com/wetransferplatform)
7 |
8 | ### 3.0.1
9 | - Merge release 3.0.0 into master ([#138](https://github.com/WeTransfer/Mocker/pull/138)) via [@wetransferplatform](https://github.com/wetransferplatform)
10 | - Add extra capabilities for on request handling ([#139](https://github.com/WeTransfer/Mocker/pull/139)) via [@AvdLee](https://github.com/AvdLee)
11 |
12 | ### 3.0.0
13 | - Revert breaking change and add `OnRequestHandler` ([#135](https://github.com/WeTransfer/Mocker/pull/135)) via [@AvdLee](https://github.com/AvdLee)
14 | - Support collection types as a top level object ([#125](https://github.com/WeTransfer/Mocker/pull/125)) via [@batuhansk](https://github.com/batuhansk)
15 | - Update README.md ([#128](https://github.com/WeTransfer/Mocker/pull/128)) via [@farrasdoko](https://github.com/farrasdoko)
16 | - Merge release 2.7.0 into master ([#127](https://github.com/WeTransfer/Mocker/pull/127)) via [@wetransferplatform](https://github.com/wetransferplatform)
17 | - Change: improve read me ([#124](https://github.com/WeTransfer/Mocker/pull/124)) via [@stavares843](https://github.com/stavares843)
18 | - Fixing CI for the new restructure of lanes ([#126](https://github.com/WeTransfer/Mocker/pull/126)) via [@AvdLee](https://github.com/AvdLee)
19 | - Merge release 2.6.0 into master ([#122](https://github.com/WeTransfer/Mocker/pull/122)) via [@wetransferplatform](https://github.com/wetransferplatform)
20 |
21 | ### 2.7.0
22 | - Support collection types as a top level object ([#125](https://github.com/WeTransfer/Mocker/pull/125)) via [@batuhansk](https://github.com/batuhansk)
23 | - Fixing CI for the new restructure of lanes ([#126](https://github.com/WeTransfer/Mocker/pull/126)) via [@AvdLee](https://github.com/AvdLee)
24 | - Change: improve read me ([#124](https://github.com/WeTransfer/Mocker/pull/124)) via [@stavares843](https://github.com/stavares843)
25 | - Merge release 2.6.0 into master ([#122](https://github.com/WeTransfer/Mocker/pull/122)) via [@wetransferplatform](https://github.com/wetransferplatform)
26 |
27 | ### 2.6.0
28 | - Add option to create a custom data type ([#121](https://github.com/WeTransfer/Mocker/pull/121)) via [@alexanderwe](https://github.com/alexanderwe)
29 | - Enable swift PM tests on Linux and macOS ([#118](https://github.com/WeTransfer/Mocker/pull/118)) via [@vox-humana](https://github.com/vox-humana)
30 | - Merge release 2.5.6 into master ([#117](https://github.com/WeTransfer/Mocker/pull/117)) via [@wetransferplatform](https://github.com/wetransferplatform)
31 |
32 | ### 2.5.6
33 | - Linux support ([#116](https://github.com/WeTransfer/Mocker/pull/116)) via [@vox-humana](https://github.com/vox-humana)
34 | - Adds Raphael as a code owner ([#114](https://github.com/WeTransfer/Mocker/pull/114)) via [@kairadiagne](https://github.com/kairadiagne)
35 | - Update README.md ([#113](https://github.com/WeTransfer/Mocker/pull/113)) via [@hawflakes](https://github.com/hawflakes)
36 | - Update README.md ([#112](https://github.com/WeTransfer/Mocker/pull/112)) via [@amdprophet](https://github.com/amdprophet)
37 | - Merge release 2.5.5 into master ([#111](https://github.com/WeTransfer/Mocker/pull/111)) via [@wetransferplatform](https://github.com/wetransferplatform)
38 |
39 | ### 2.5.5
40 | - Allow subclassing the MockingURLProtocol, fallback for HTTP Body ([#109](https://github.com/WeTransfer/Mocker/pull/109)) via [@AvdLee](https://github.com/AvdLee)
41 |
42 | ### 2.5.4
43 | - Improve test expressivity ([#101](https://github.com/WeTransfer/Mocker/pull/101)) via [@BasThomas](https://github.com/BasThomas)
44 | - Installation via CocoaPods is Broken ([#94](https://github.com/WeTransfer/Mocker/issues/94)) via [@BasThomas](https://github.com/BasThomas)
45 | - Update to latest pod version in README ([#103](https://github.com/WeTransfer/Mocker/pull/103)) via [@BasThomas](https://github.com/BasThomas)
46 | - Update CI ([#99](https://github.com/WeTransfer/Mocker/pull/99)) via [@kairadiagne](https://github.com/kairadiagne)
47 | - Merge release 2.5.3 into master ([#96](https://github.com/WeTransfer/Mocker/pull/96)) via [@wetransferplatform](https://github.com/wetransferplatform)
48 |
49 | ### 2.5.3
50 | - Make sure file extension mocks are matching correctly ([#95](https://github.com/WeTransfer/Mocker/pull/95)) via [@AvdLee](https://github.com/AvdLee)
51 | - Replace occurrences of internal `.data` with `Data(contentsOf:)` ([#92](https://github.com/WeTransfer/Mocker/pull/92)) via [@rogerluan](https://github.com/rogerluan)
52 | - Merge release 2.5.2 into master ([#91](https://github.com/WeTransfer/Mocker/pull/91)) via [@wetransferplatform](https://github.com/wetransferplatform)
53 |
54 | ### 2.5.2
55 | - Merge release 2.5.2 into master ([#90](https://github.com/WeTransfer/Mocker/pull/90)) via [@wetransferplatform](https://github.com/wetransferplatform)
56 | - Fixing usage of XCTest framework ([#89](https://github.com/WeTransfer/Mocker/pull/89)) via [@letatas](https://github.com/letatas)
57 | - Merge release 2.5.1 into master ([#87](https://github.com/WeTransfer/Mocker/pull/87)) via [@wetransferplatform](https://github.com/wetransferplatform)
58 |
59 | ### 2.5.2
60 | - Fixing usage of XCTest framework ([#89](https://github.com/WeTransfer/Mocker/pull/89)) via [@letatas](https://github.com/letatas)
61 | - Merge release 2.5.1 into master ([#87](https://github.com/WeTransfer/Mocker/pull/87)) via [@wetransferplatform](https://github.com/wetransferplatform)
62 |
63 | ### 2.5.1
64 | - Fix tests and make sure the new opt-in mode is working with existing logic ([#86](https://github.com/WeTransfer/Mocker/pull/86)) via [@AvdLee](https://github.com/AvdLee)
65 | - Merge release 2.5.0 into master ([#85](https://github.com/WeTransfer/Mocker/pull/85)) via [@wetransferplatform](https://github.com/wetransferplatform)
66 |
67 | ### 2.5.0
68 | - Feat: Global mode to choose only to mock registered routes ([#84](https://github.com/WeTransfer/Mocker/pull/84)) via [@letatas](https://github.com/letatas)
69 | - Update README.md ([#74](https://github.com/WeTransfer/Mocker/pull/74)) via [@airowe](https://github.com/airowe)
70 |
71 | ### 2.3.0
72 | - Add XCTest extensions ([#57](https://github.com/WeTransfer/Mocker/pull/57)) via [@AvdLee](https://github.com/AvdLee)
73 | - Merge release 2.2.0 into master ([#55](https://github.com/WeTransfer/Mocker/pull/55)) via [@WeTransferBot](https://github.com/WeTransferBot)
74 |
75 | ### 2.2.0
76 | - ignoring query example swap i/o url ([#54](https://github.com/WeTransfer/Mocker/pull/54)) via [@GeRryCh](https://github.com/GeRryCh)
77 | - Update README.md ([#53](https://github.com/WeTransfer/Mocker/pull/53)) via [@mtsrodrigues](https://github.com/mtsrodrigues)
78 | - mixing in the ability to send an explicit error from a mock response ([#52](https://github.com/WeTransfer/Mocker/pull/52)) via [@heckj](https://github.com/heckj)
79 | - Document that onRequest and completion must be set before calling register() ([#47](https://github.com/WeTransfer/Mocker/pull/47)) via [@marcetcheverry](https://github.com/marcetcheverry)
80 | - Update readme for Alamofire 5 ([#48](https://github.com/WeTransfer/Mocker/pull/48)) via [@AvdLee](https://github.com/AvdLee)
81 | - Merge release 2.1.0 into master ([#46](https://github.com/WeTransfer/Mocker/pull/46)) via [@WeTransferBot](https://github.com/WeTransferBot)
82 |
83 | ### 2.1.0
84 | - Enable post body checks ([#41](https://github.com/WeTransfer/Mocker/pull/41)) via @AvdLee
85 | - Merge release 2.0.2 into master ([#40](https://github.com/WeTransfer/Mocker/pull/40))
86 |
87 | ### 2.0.2
88 |
89 | - Make use of the shared SwiftLint script ([#39](https://github.com/WeTransfer/Mocker/pull/39)) via @AvdLee
90 | - Enable tag releasing ([#38](https://github.com/WeTransfer/Mocker/pull/38)) via @AvdLee
91 |
92 | ### 2.0.1
93 |
94 | - Switch over to Danger-Swift & Bitrise ([#34](https://github.com/WeTransfer/Mocker/pull/34)) via @AvdLee
95 | - Fix important mismatch for getting the right mock ([#31](https://github.com/WeTransfer/Mocker/pull/31)) via @AvdLee
96 |
97 | ### 2.0.0
98 | - A new completion callback can be set on `Mock` to use for expectation fulfilling once a `Mock` is completed.
99 | - A new onRequest callback can be set on `Mock` to use for expectation fulfilling once a `Mock` is requested.
100 | - Updated to Swift 5.0
101 | - Only dispatch to the background queue if needed
102 | - Correctly handle cancellation of delayed responses
103 | - Adding and reading mocks is now thread safe by using a Dispatch Semaphore
104 | - Add support for using Swift Package Manager
105 | - Improved checking for Mocks using `URLRequest`.
106 |
107 | ### 1.3.0
108 | - Updated to Swift 4.2
109 |
110 | ### 1.2.1 (2018-09-11)
111 | - Improved CI
112 | - Better matching Mocks based on `absoluteString`
113 | - Migrated to Swift 4.1
114 |
115 | ### 1.2.0 (2018-02-09)
116 | - Ignoring query path for mocks
117 | - Missing mocks no longer break tests (removed fatalError)
118 | - Improved SwiftLint implementation
119 |
120 | ### 1.1.0 (2017-11-03)
121 | - Adds support for delayed responses
122 | - Adds support for ignoring URLs
123 | - Adds support for redirects
124 | - Migrated to Swift 4.0
125 |
126 | ### 1.0 (2017-08-11)
127 |
128 | - First public release! 🎉
129 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | ci_gems_path = File.join(File.dirname(__FILE__), "Submodules/WeTransfer-iOS-CI/Gemfile")
4 | eval_gemfile(ci_gems_path) if File.exist?(ci_gems_path)
5 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.5
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import PackageDescription
5 |
6 | let package = Package(
7 | name: "Mocker",
8 | platforms: [
9 | .macOS(.v10_15),
10 | .iOS(.v11),
11 | .tvOS(.v12),
12 | .watchOS(.v6)],
13 | products: [
14 | .library(name: "Mocker", targets: ["Mocker"])
15 | ],
16 | targets: [
17 | .target(
18 | name: "Mocker"
19 | ),
20 | .testTarget(
21 | name: "MockerTests",
22 | dependencies: ["Mocker"],
23 | resources: [
24 | .process("Resources")
25 | ]
26 | )
27 | ],
28 | swiftLanguageVersions: [.v5])
29 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | Mocker is a library written in Swift which makes it possible to mock data requests using a custom `URLProtocol`.
17 |
18 | - [Features](#features)
19 | - [Requirements](#requirements)
20 | - [Usage](#usage)
21 | - [Activating the Mocker](#activating-the-mocker)
22 | - [Custom URLSessions](#custom-urlsessions)
23 | - [Alamofire](#alamofire)
24 | - [Register Mocks](#register-mocks)
25 | - [Create your mocked data](#create-your-mocked-data)
26 | - [JSON Requests](#json-requests)
27 | - [File extensions](#file-extensions)
28 | - [Custom HEAD and GET response](#custom-head-and-get-response)
29 | - [Delayed responses](#delayed-responses)
30 | - [Redirect responses](#redirect-responses)
31 | - [Ignoring URLs](#ignoring-urls)
32 | - [Mock callbacks](#mock-callbacks)
33 | - [Unregister Mocks](#unregister-mocks)
34 | - [Clear all registered mocks](#clear-all-registered-mocks)
35 | - [Communication](#communication)
36 | - [Installation](#installation)
37 | - [Release Notes](#release-notes)
38 | - [License](#license)
39 |
40 | ## Features
41 | _Run all your data request unit tests offline_ 🎉
42 |
43 | - [x] Create mocked data requests based on an URL
44 | - [x] Create mocked data requests based on a file extension
45 | - [x] Works with `URLSession` using a custom protocol class
46 | - [x] Supports popular frameworks like `Alamofire`
47 |
48 | ## Usage
49 |
50 | Unit tests are written for the `Mocker` which can help you to see how it works.
51 |
52 | ### Activating the Mocker
53 | The mocker will automatically be activated for the default URL loading system like `URLSession.shared` after you've registered your first `Mock`.
54 |
55 | ##### Custom URLSessions
56 | To make it work with your custom `URLSession`, the `MockingURLProtocol` needs to be registered:
57 |
58 | ```swift
59 | let configuration = URLSessionConfiguration.default
60 | configuration.protocolClasses = [MockingURLProtocol.self]
61 | let urlSession = URLSession(configuration: configuration)
62 | ```
63 |
64 | ##### Alamofire
65 | Quite similar like registering on a custom `URLSession`.
66 |
67 | ```swift
68 | let configuration = URLSessionConfiguration.af.default
69 | configuration.protocolClasses = [MockingURLProtocol.self]
70 | let sessionManager = Alamofire.Session(configuration: configuration)
71 | ```
72 |
73 | ### Register Mocks
74 | ##### Create your mocked data
75 | It's recommended to create a class with all your mocked data accessible. An example of this can be found in the unit tests of this project:
76 |
77 | ```swift
78 | public final class MockedData {
79 | public static let botAvatarImageResponseHead: Data = try! Data(contentsOf: Bundle(for: MockedData.self).url(forResource: "Resources/Responses/bot-avatar-image-head", withExtension: "data")!)
80 | public static let botAvatarImageFileUrl: URL = Bundle(for: MockedData.self).url(forResource: "wetransfer_bot_avater", withExtension: "png")!
81 | public static let exampleJSON: URL = Bundle(for: MockedData.self).url(forResource: "Resources/JSON Files/example", withExtension: "json")!
82 | }
83 | ```
84 |
85 | ##### JSON Requests
86 | ``` swift
87 | let originalURL = URL(string: "https://www.wetransfer.com/example.json")!
88 |
89 | let mock = Mock(url: originalURL, contentType: .json, statusCode: 200, data: [
90 | .get : try! Data(contentsOf: MockedData.exampleJSON) // Data containing the JSON response
91 | ])
92 | mock.register()
93 |
94 | URLSession.shared.dataTask(with: originalURL) { (data, response, error) in
95 | guard let data = data, let jsonDictionary = (try? JSONSerialization.jsonObject(with: data, options: [])) as? [String: Any] else {
96 | return
97 | }
98 |
99 | // jsonDictionary contains your JSON sample file data
100 | // ..
101 |
102 | }.resume()
103 | ```
104 |
105 | ##### Empty Responses
106 | ``` swift
107 | let originalURL = URL(string: "https://www.wetransfer.com/api/foobar")!
108 | var request = URLRequest(url: originalURL)
109 | request.httpMethod = "PUT"
110 |
111 | let mock = Mock(request: request, statusCode: 204)
112 | mock.register()
113 |
114 | URLSession.shared.dataTask(with: originalURL) { (data, response, error) in
115 | // ....
116 | }.resume()
117 | ```
118 |
119 | ##### Ignoring the query
120 | Some URLs like authentication URLs contain timestamps or UUIDs in the query. To mock these you can ignore the Query for a certain URL:
121 |
122 | ``` swift
123 | /// Would transform to "https://www.example.com/api/authentication" for example.
124 | let originalURL = URL(string: "https://www.example.com/api/authentication?oauth_timestamp=151817037")!
125 |
126 | let mock = Mock(url: originalURL, ignoreQuery: true, contentType: .json, statusCode: 200, data: [
127 | .get : try! Data(contentsOf: MockedData.exampleJSON) // Data containing the JSON response
128 | ])
129 | mock.register()
130 |
131 | URLSession.shared.dataTask(with: originalURL) { (data, response, error) in
132 | guard let data = data, let jsonDictionary = (try? JSONSerialization.jsonObject(with: data, options: [])) as? [String: Any] else {
133 | return
134 | }
135 |
136 | // jsonDictionary contains your JSON sample file data
137 | // ..
138 |
139 | }.resume()
140 | ```
141 |
142 | ##### File extensions
143 | ```swift
144 | let imageURL = URL(string: "https://www.wetransfer.com/sample-image.png")!
145 |
146 | Mock(fileExtensions: "png", contentType: .imagePNG, statusCode: 200, data: [
147 | .get: try! Data(contentsOf: MockedData.botAvatarImageFileUrl)
148 | ]).register()
149 |
150 | URLSession.shared.dataTask(with: imageURL) { (data, response, error) in
151 | let botAvatarImage: UIImage = UIImage(data: data!)! // This is the image from your resources.
152 | }.resume()
153 | ```
154 |
155 | ##### Custom HEAD and GET response
156 | ```swift
157 | let exampleURL = URL(string: "https://www.wetransfer.com/api/endpoint")!
158 |
159 | Mock(url: exampleURL, contentType: .json, statusCode: 200, data: [
160 | .head: try! Data(contentsOf: MockedData.headResponse),
161 | .get: try! Data(contentsOf: MockedData.exampleJSON)
162 | ]).register()
163 |
164 | URLSession.shared.dataTask(with: exampleURL) { (data, response, error) in
165 | // data is your mocked data
166 | }.resume()
167 | ```
168 |
169 | ##### Custom DataType
170 | In addition to the already build in static `DataType` implementations it is possible to create custom ones that will be used as the value to the `Content-Type` header key.
171 |
172 | ```swift
173 | let xmlURL = URL(string: "https://www.wetransfer.com/sample-xml.xml")!
174 |
175 | Mock(fileExtensions: "png", contentType: .init(name: "xml", headerValue: "text/xml"), statusCode: 200, data: [
176 | .get: try! Data(contentsOf: MockedData.sampleXML)
177 | ]).register()
178 |
179 | URLSession.shared.dataTask(with: xmlURL) { (data, response, error) in
180 | let sampleXML: Data = data // This is the xml from your resources.
181 | }.resume(
182 | ```
183 |
184 |
185 | ##### Delayed responses
186 | Sometimes you want to test if the cancellation of requests is working. In that case, the mocked request should not finish immediately and you need a delay. This can be added easily:
187 |
188 | ```swift
189 | let exampleURL = URL(string: "https://www.wetransfer.com/api/endpoint")!
190 |
191 | var mock = Mock(url: exampleURL, contentType: .json, statusCode: 200, data: [
192 | .head: try! Data(contentsOf: MockedData.headResponse),
193 | .get: try! Data(contentsOf: MockedData.exampleJSON)
194 | ])
195 | mock.delay = DispatchTimeInterval.seconds(5)
196 | mock.register()
197 | ```
198 |
199 | ##### Redirect responses
200 | Sometimes you want to mock short URLs or other redirect URLs. This is possible by saving the response and mocking the redirect location, which can be found inside the response:
201 |
202 | ```
203 | Date: Tue, 10 Oct 2017 07:28:33 GMT
204 | Location: https://wetransfer.com/redirect
205 | ```
206 |
207 | By creating a mock for the short URL and the redirect URL, you can mock redirect and test this behavior:
208 |
209 | ```swift
210 | let urlWhichRedirects: URL = URL(string: "https://we.tl/redirect")!
211 | Mock(url: urlWhichRedirects, contentType: .html, statusCode: 200, data: [.get: try! Data(contentsOf: MockedData.redirectGET)]).register()
212 | Mock(url: URL(string: "https://wetransfer.com/redirect")!, contentType: .json, statusCode: 200, data: [.get: try! Data(contentsOf: MockedData.exampleJSON)]).register()
213 | ```
214 |
215 | ##### Ignoring URLs
216 | As the Mocker catches all URLs by default when registered, you might end up with a `fatalError` thrown in cases you don't need a mocked request. In that case, you can ignore the URL:
217 |
218 | ```swift
219 | let ignoredURL = URL(string: "https://www.wetransfer.com")!
220 |
221 | // Ignore any requests that exactly match the URL
222 | Mocker.ignore(ignoredURL)
223 |
224 | // Ignore any requests that match the URL, with any query parameters
225 | // e.g. https://www.wetransfer.com?foo=bar would be ignored
226 | Mocker.ignore(ignoredURL, matchType: .ignoreQuery)
227 |
228 | // Ignore any requests that begin with the URL
229 | // e.g. https://www.wetransfer.com/api/v1 would be ignored
230 | Mocker.ignore(ignoredURL, matchType: .prefix)
231 | ```
232 |
233 | However, if you need the Mocker to catch only mocked URLs and ignore every other URL, you can set the `mode` attribute to `.optin`.
234 |
235 | ```swift
236 | Mocker.mode = .optin
237 | ```
238 |
239 | If you want to set the original mode back, you have just to set it to `.optout`.
240 |
241 | ```swift
242 | Mocker.mode = .optout
243 | ```
244 |
245 | ##### Mock errors
246 |
247 | You can request a `Mock` to return an error, allowing testing of error handling.
248 |
249 | ```swift
250 | Mock(url: originalURL, contentType: .json, statusCode: 500, data: [.get: Data()],
251 | requestError: TestExampleError.example).register()
252 |
253 | URLSession.shared.dataTask(with: originalURL) { (data, urlresponse, err) in
254 | XCTAssertNil(data)
255 | XCTAssertNil(urlresponse)
256 | XCTAssertNotNil(err)
257 | if let err = err {
258 | // there's not a particularly elegant way to verify an instance
259 | // of an error, but this is a convenient workaround for testing
260 | // purposes
261 | XCTAssertEqual("example", String(describing: err))
262 | }
263 |
264 | expectation.fulfill()
265 | }.resume()
266 | ```
267 |
268 | ##### Mock callbacks
269 | You can register on `Mock` callbacks to make testing easier.
270 |
271 | ```swift
272 | var mock = Mock(url: request.url!, contentType: .json, statusCode: 200, data: [.post: Data()])
273 | mock.onRequestHandler = OnRequestHandler(httpBodyType: [[String:String]].self, callback: { request, postBodyArguments in
274 | XCTAssertEqual(request.url, mock.request.url)
275 | XCTAssertEqual(expectedParameters, postBodyArguments)
276 | onRequestExpectation.fulfill()
277 | })
278 | mock.completion = {
279 | endpointIsCalledExpectation.fulfill()
280 | }
281 | mock.register()
282 | ```
283 |
284 | ##### Mock expectations
285 | Instead of setting the `completion` and `onRequest` you can also make use of expectations:
286 |
287 | ```swift
288 | var mock = Mock(url: url, contentType: .json, statusCode: 200, data: [.get: Data()])
289 | let requestExpectation = expectationForRequestingMock(&mock)
290 | let completionExpectation = expectationForCompletingMock(&mock)
291 | mock.register()
292 |
293 | URLSession.shared.dataTask(with: URLRequest(url: url)).resume()
294 |
295 | wait(for: [requestExpectation, completionExpectation], timeout: 2.0)
296 | ```
297 |
298 | ### Unregister Mocks
299 | ##### Clear all registered mocks
300 | You can clear all registered mocks:
301 |
302 | ```swift
303 | Mocker.removeAll()
304 | ```
305 |
306 | ## Communication
307 |
308 | - If you **found a bug**, open an issue.
309 | - If you **have a feature request**, open an issue.
310 | - If you **want to contribute**, submit a pull request.
311 |
312 | ## Installation
313 |
314 | ### Carthage
315 |
316 | [Carthage](https://github.com/Carthage/Carthage) is a decentralized dependency manager that builds your dependencies and provides you with binary frameworks.
317 |
318 | You can install Carthage with [Homebrew](http://brew.sh/) using the following command:
319 |
320 | ```bash
321 | $ brew update
322 | $ brew install carthage
323 | ```
324 |
325 | To integrate Mocker into your Xcode project using Carthage, specify it in your `Cartfile`:
326 |
327 | ```ogdl
328 | github "WeTransfer/Mocker" ~> 3.0.0
329 | ```
330 |
331 | Run `carthage update` to build the framework and drag the built `Mocker.framework` into your Xcode project.
332 |
333 | ### Swift Package Manager
334 |
335 | The [Swift Package Manager](https://swift.org/package-manager/) is a tool for managing the distribution of Swift code. It’s integrated with the Swift build system to automate the process of downloading, compiling, and linking dependencies.
336 |
337 | #### Manifest File
338 |
339 | Add Mocker as a package to your `Package.swift` file and then specify it as a dependency of the Target in which you wish to use it.
340 |
341 | ```swift
342 | import PackageDescription
343 |
344 | let package = Package(
345 | name: "MyProject",
346 | platforms: [
347 | .macOS(.v10_15)
348 | ],
349 | dependencies: [
350 | .package(url: "https://github.com/WeTransfer/Mocker.git", .upToNextMajor(from: "3.0.0"))
351 | ],
352 | targets: [
353 | .target(
354 | name: "MyProject",
355 | dependencies: ["Mocker"]),
356 | .testTarget(
357 | name: "MyProjectTests",
358 | dependencies: ["MyProject"]),
359 | ]
360 | )
361 | ```
362 |
363 | #### Xcode
364 |
365 | To add Mocker as a [dependency](https://developer.apple.com/documentation/xcode/adding_package_dependencies_to_your_app) to your Xcode project, select *File > Swift Packages > Add Package Dependency* and enter the repository URL.
366 |
367 | #### Resolving Build Errors
368 | If you get the following error: *cannot find auto-link library XCTest and XCTestSwiftSupport*, set the following property under Build Options from No to Yes.
369 | ENABLE_TESTING_SEARCH_PATHS to YES
370 |
371 | ### Manually
372 |
373 | If you prefer not to use any of the aforementioned dependency managers, you can integrate Mocker into your project manually.
374 |
375 | #### Embedded Framework
376 |
377 | - Open up Terminal, `cd` into your top-level project directory, and run the following command "if" your project is not initialized as a git repository:
378 |
379 | ```bash
380 | $ git init
381 | ```
382 |
383 | - Add Mocker as a git [submodule](http://git-scm.com/docs/git-submodule) by running the following command:
384 |
385 | ```bash
386 | $ git submodule add https://github.com/WeTransfer/Mocker.git
387 | ```
388 |
389 | - Open the new `Mocker ` folder, and drag the `Mocker.xcodeproj` into the Project Navigator of your application's Xcode project.
390 |
391 | > It should appear nested underneath your application's blue project icon. Whether it is above or below all the other Xcode groups does not matter.
392 |
393 | - Select the `Mocker.xcodeproj` in the Project Navigator and verify the deployment target matches that of your application target.
394 | - Next, select your application project in the Project Navigator (blue project icon) to navigate to the target configuration window and select the application target under the "Targets" heading in the sidebar.
395 | - In the tab bar at the top of that window, open the "General" panel.
396 | - Click on the `+` button under the "Embedded Binaries" section.
397 | - Select `Mocker.framework`.
398 | - And that's it!
399 |
400 | > The `Mocker.framework` is automagically added as a target dependency, linked framework and embedded framework in a copy files build phase which is all you need to build on the simulator and a device.
401 |
402 | ---
403 |
404 | ## Release Notes
405 |
406 | See [CHANGELOG.md](https://github.com/WeTransfer/Mocker/blob/master/Changelog.md) for a list of changes.
407 |
408 | ## License
409 |
410 | Mocker is available under the MIT license. See the LICENSE file for more info.
411 |
--------------------------------------------------------------------------------
/Sources/Mocker/Mock+DataType.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Mock+DataType.swift
3 | // Mocker
4 | //
5 | // Created by Weiß, Alexander on 26.07.22.
6 | // Copyright © 2022 WeTransfer. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | extension Mock {
12 | /// The types of content of a request. Will be used as Content-Type header inside a `Mock`.
13 | public struct DataType: Sendable {
14 |
15 | /// Name of the data type.
16 | public let name: String
17 |
18 | /// The header value of the data type.
19 | public let headerValue: String
20 |
21 | public init(name: String, headerValue: String) {
22 | self.name = name
23 | self.headerValue = headerValue
24 | }
25 | }
26 | }
27 |
28 | extension Mock.DataType {
29 | public static let json = Mock.DataType(name: "json", headerValue: "application/json; charset=utf-8")
30 | public static let html = Mock.DataType(name: "html", headerValue: "text/html; charset=utf-8")
31 | public static let imagePNG = Mock.DataType(name: "imagePNG", headerValue: "image/png")
32 | public static let pdf = Mock.DataType(name: "pdf", headerValue: "application/pdf")
33 | public static let mp4 = Mock.DataType(name: "mp4", headerValue: "video/mp4")
34 | public static let zip = Mock.DataType(name: "zip", headerValue: "application/zip")
35 | }
36 |
--------------------------------------------------------------------------------
/Sources/Mocker/Mock.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Mock.swift
3 | // Rabbit
4 | //
5 | // Created by Antoine van der Lee on 04/05/2017.
6 | // Copyright © 2017 WeTransfer. All rights reserved.
7 | //
8 | // Mocker is only used for tests. In tests we don't even check on this SwiftLint warning, but Mocker is available through Rabbit for usage out of Rabbit. Disable for this case.
9 | // swiftlint:disable force_unwrapping
10 |
11 | import Foundation
12 | import XCTest
13 | #if canImport(FoundationNetworking)
14 | import FoundationNetworking
15 | #endif
16 |
17 | /// A Mock which can be used for mocking data requests with the `Mocker` by calling `Mocker.register(...)`.
18 | public struct Mock: Equatable {
19 |
20 | /// HTTP method definitions.
21 | ///
22 | /// See https://tools.ietf.org/html/rfc7231#section-4.3
23 | public enum HTTPMethod: String, Sendable {
24 | case options = "OPTIONS"
25 | case get = "GET"
26 | case head = "HEAD"
27 | case post = "POST"
28 | case put = "PUT"
29 | case patch = "PATCH"
30 | case delete = "DELETE"
31 | case trace = "TRACE"
32 | case connect = "CONNECT"
33 | }
34 |
35 | public typealias OnRequest = (_ request: URLRequest, _ httpBodyArguments: [String: Any]?) -> Void
36 |
37 | /// The type of the data which designates the Content-Type header.
38 | @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.")
39 | public var dataType: DataType {
40 | return contentType!
41 | }
42 |
43 | /// The type of the data which designates the Content-Type header. If set to `nil`, no Content-Type header is added to the headers.
44 | public let contentType: DataType?
45 |
46 | /// If set, the error that URLProtocol will report as a result rather than returning data from the mock
47 | public let requestError: Error?
48 |
49 | /// The headers to send back with the response.
50 | public let headers: [String: String]
51 |
52 | /// The HTTP status code to return with the response.
53 | public let statusCode: Int
54 |
55 | /// The URL value generated based on the Mock data. Force unwrapped on purpose. If you access this URL while it's not set, this is a programming error.
56 | public var url: URL {
57 | if urlToMock == nil && !data.keys.contains(.get) {
58 | assertionFailure("For non GET mocks you should use the `request` property so the HTTP method is set.")
59 | }
60 | return urlToMock ?? generatedURL
61 | }
62 |
63 | /// The URL to mock as set implicitely from the init.
64 | private let urlToMock: URL?
65 |
66 | /// The URL generated from all the data set on this mock.
67 | private let generatedURL: URL
68 |
69 | /// The `URLRequest` to use if you did not set a specific URL.
70 | public let request: URLRequest
71 |
72 | /// If `true`, checking the URL will ignore the query and match only for the scheme, host and path.
73 | public let ignoreQuery: Bool
74 |
75 | /// The file extensions to match for.
76 | public let fileExtensions: [String]?
77 |
78 | /// The data which will be returned as the response based on the HTTP Method.
79 | private let data: [HTTPMethod: Data]
80 |
81 | /// Add a delay to a certain mock, which makes the response returned later.
82 | public var delay: DispatchTimeInterval?
83 |
84 | /// Allow response cache.
85 | public var cacheStoragePolicy: URLCache.StoragePolicy = .notAllowed
86 |
87 | /// The callback which will be executed everytime this `Mock` was completed. Can be used within unit tests for validating that a request has been executed. The callback must be set before calling `register`.
88 | public var completion: (() -> Void)?
89 |
90 | /// The callback which will be executed everytime this `Mock` was started. Can be used within unit tests for validating that a request has been started. The callback must be set before calling `register`.
91 | @available(*, deprecated, message: "Use `onRequestHandler` instead.")
92 | public var onRequest: OnRequest? {
93 | set {
94 | onRequestHandler = OnRequestHandler(legacyCallback: newValue)
95 | }
96 | get {
97 | onRequestHandler?.legacyCallback
98 | }
99 | }
100 |
101 | /// 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`.
102 | public var onRequestHandler: OnRequestHandler?
103 |
104 | /// Can only be set internally as it's used by the `expectationForRequestingMock(_:)` method.
105 | var onRequestExpectation: XCTestExpectation?
106 |
107 | /// Can only be set internally as it's used by the `expectationForCompletingMock(_:)` method.
108 | var onCompletedExpectation: XCTestExpectation?
109 |
110 | 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) {
111 | guard data.count > 0 else {
112 | preconditionFailure("At least one entry is required in the data dictionary")
113 | }
114 |
115 | self.urlToMock = url
116 | let generatedURL = URL(string: "https://mocked.wetransfer.com/\(contentType?.name ?? "no-content")/\(statusCode)/\(data.keys.first!.rawValue)")!
117 | self.generatedURL = generatedURL
118 | var request = URLRequest(url: url ?? generatedURL)
119 | request.httpMethod = data.keys.first!.rawValue
120 | self.request = request
121 | self.ignoreQuery = ignoreQuery
122 | self.requestError = requestError
123 | self.contentType = contentType
124 | self.statusCode = statusCode
125 | self.data = data
126 | self.cacheStoragePolicy = cacheStoragePolicy
127 |
128 | var headers = additionalHeaders
129 | if let contentType = contentType {
130 | headers["Content-Type"] = contentType.headerValue
131 | }
132 | self.headers = headers
133 |
134 | self.fileExtensions = fileExtensions?.map({ $0.replacingOccurrences(of: ".", with: "") })
135 | }
136 |
137 | /// Creates a `Mock` for the given data type. The mock will be automatically matched based on a URL created from the given parameters.
138 | ///
139 | /// - Parameters:
140 | /// - dataType: The type of the data which designates the Content-Type header.
141 | /// - statusCode: The HTTP status code to return with the response.
142 | /// - data: The data which will be returned as the response based on the HTTP Method.
143 | /// - additionalHeaders: Additional headers to be added to the response.
144 | @available(*, deprecated, renamed: "init(contentType:statusCode:data:additionalHeaders:)")
145 | public init(dataType: DataType, statusCode: Int, data: [HTTPMethod: Data], additionalHeaders: [String: String] = [:]) {
146 | self.init(
147 | url: nil,
148 | contentType: dataType,
149 | statusCode: statusCode,
150 | data: data,
151 | additionalHeaders: additionalHeaders,
152 | fileExtensions: nil
153 | )
154 | }
155 |
156 | /// Creates a `Mock` for the given content type. The mock will be automatically matched based on a URL created from the given parameters.
157 | ///
158 | /// - Parameters:
159 | /// - 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.
160 | /// - statusCode: The HTTP status code to return with the response.
161 | /// - data: The data which will be returned as the response based on the HTTP Method.
162 | /// - additionalHeaders: Additional headers to be added to the response.
163 | public init(contentType: DataType?, statusCode: Int, data: [HTTPMethod: Data], additionalHeaders: [String: String] = [:]) {
164 | self.init(
165 | url: nil,
166 | contentType: contentType,
167 | statusCode: statusCode,
168 | data: data,
169 | additionalHeaders: additionalHeaders,
170 | fileExtensions: nil
171 | )
172 | }
173 |
174 | /// Creates a `Mock` for the given URL.
175 | ///
176 | /// - Parameters:
177 | /// - url: The URL to match for and to return the mocked data for.
178 | /// - ignoreQuery: If `true`, checking the URL will ignore the query and match only for the scheme, host and path. Defaults to `false`.
179 | /// - cacheStoragePolicy: The caching strategy. Defaults to `notAllowed`.
180 | /// - dataType: The type of the data which designates the Content-Type header.
181 | /// - statusCode: The HTTP status code to return with the response.
182 | /// - data: The data which will be returned as the response based on the HTTP Method.
183 | /// - additionalHeaders: Additional headers to be added to the response.
184 | /// - requestError: If provided, the URLSession will report the passed error rather than returning data. Defaults to `nil`.
185 | @available(*, deprecated, renamed: "init(url:ignoreQuery:cacheStoragePolicy:contentType:statusCode:data:additionalHeaders:requestError:)")
186 | public init(url: URL, ignoreQuery: Bool = false, cacheStoragePolicy: URLCache.StoragePolicy = .notAllowed, dataType: DataType, statusCode: Int, data: [HTTPMethod: Data], additionalHeaders: [String: String] = [:], requestError: Error? = nil) {
187 | self.init(
188 | url: url,
189 | ignoreQuery: ignoreQuery,
190 | cacheStoragePolicy: cacheStoragePolicy,
191 | contentType: dataType,
192 | statusCode: statusCode,
193 | data: data,
194 | requestError: requestError,
195 | additionalHeaders: additionalHeaders,
196 | fileExtensions: nil
197 | )
198 | }
199 |
200 | /// Creates a `Mock` for the given URL.
201 | ///
202 | /// - Parameters:
203 | /// - url: The URL to match for and to return the mocked data for.
204 | /// - ignoreQuery: If `true`, checking the URL will ignore the query and match only for the scheme, host and path. Defaults to `false`.
205 | /// - cacheStoragePolicy: The caching strategy. Defaults to `notAllowed`.
206 | /// - 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.
207 | /// - statusCode: The HTTP status code to return with the response.
208 | /// - data: The data which will be returned as the response based on the HTTP Method.
209 | /// - additionalHeaders: Additional headers to be added to the response.
210 | /// - requestError: If provided, the URLSession will report the passed error rather than returning data. Defaults to `nil`.
211 | 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) {
212 | self.init(
213 | url: url,
214 | ignoreQuery: ignoreQuery,
215 | cacheStoragePolicy: cacheStoragePolicy,
216 | contentType: contentType,
217 | statusCode: statusCode,
218 | data: data,
219 | requestError: requestError,
220 | additionalHeaders: additionalHeaders,
221 | fileExtensions: nil
222 | )
223 | }
224 |
225 | /// Creates a `Mock` for the given file extensions. The mock will only be used for urls matching the extension.
226 | ///
227 | /// - Parameters:
228 | /// - fileExtensions: The file extension to match for.
229 | /// - dataType: The type of the data which designates the Content-Type header.
230 | /// - statusCode: The HTTP status code to return with the response.
231 | /// - data: The data which will be returned as the response based on the HTTP Method.
232 | /// - additionalHeaders: Additional headers to be added to the response.
233 | @available(*, deprecated, renamed: "init(fileExtensions:contentType:statusCode:data:additionalHeaders:)")
234 | public init(fileExtensions: String..., dataType: DataType, statusCode: Int, data: [HTTPMethod: Data], additionalHeaders: [String: String] = [:]) {
235 | self.init(
236 | url: nil,
237 | contentType: dataType,
238 | statusCode: statusCode,
239 | data: data,
240 | additionalHeaders: additionalHeaders,
241 | fileExtensions: fileExtensions
242 | )
243 | }
244 |
245 | /// Creates a `Mock` for the given file extensions. The mock will only be used for urls matching the extension.
246 | ///
247 | /// - Parameters:
248 | /// - fileExtensions: The file extension to match for.
249 | /// - 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.
250 | /// - statusCode: The HTTP status code to return with the response.
251 | /// - data: The data which will be returned as the response based on the HTTP Method.
252 | /// - additionalHeaders: Additional headers to be added to the response.
253 | public init(fileExtensions: String..., contentType: DataType? = nil, statusCode: Int, data: [HTTPMethod: Data], additionalHeaders: [String: String] = [:]) {
254 | self.init(
255 | url: nil,
256 | contentType: contentType,
257 | statusCode: statusCode,
258 | data: data,
259 | additionalHeaders: additionalHeaders,
260 | fileExtensions: fileExtensions
261 | )
262 | }
263 |
264 | /// Creates a `Mock` for the given `URLRequest`.
265 | ///
266 | /// - Parameters:
267 | /// - request: The URLRequest, from which the URL and request method is used to match for and to return the mocked data for.
268 | /// - ignoreQuery: If `true`, checking the URL will ignore the query and match only for the scheme, host and path. Defaults to `false`.
269 | /// - cacheStoragePolicy: The caching strategy. Defaults to `notAllowed`.
270 | /// - 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.
271 | /// - statusCode: The HTTP status code to return with the response.
272 | /// - data: The data which will be returned as the response. Defaults to an empty `Data` instance.
273 | /// - additionalHeaders: Additional headers to be added to the response.
274 | /// - requestError: If provided, the URLSession will report the passed error rather than returning data. Defaults to `nil`.
275 | 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) {
276 | guard let requestHTTPMethod = Mock.HTTPMethod(rawValue: request.httpMethod ?? "") else {
277 | preconditionFailure("Unexpected http method")
278 | }
279 |
280 | self.init(
281 | url: request.url,
282 | ignoreQuery: ignoreQuery,
283 | cacheStoragePolicy: cacheStoragePolicy,
284 | contentType: contentType,
285 | statusCode: statusCode,
286 | data: [requestHTTPMethod: data],
287 | requestError: requestError,
288 | additionalHeaders: additionalHeaders,
289 | fileExtensions: nil
290 | )
291 | }
292 |
293 | /// Registers the mock with the shared `Mocker`.
294 | public func register() {
295 | Mocker.register(self)
296 | }
297 |
298 | /// Returns `Data` based on the HTTP Method of the passed request.
299 | ///
300 | /// - Parameter request: The request to match data for.
301 | /// - Returns: The `Data` which matches the request. Will be `nil` if no data is registered for the request `HTTPMethod`.
302 | func data(for request: URLRequest) -> Data? {
303 | guard let requestHTTPMethod = Mock.HTTPMethod(rawValue: request.httpMethod ?? "") else { return nil }
304 | return data[requestHTTPMethod]
305 | }
306 |
307 | /// Used to compare the Mock data with the given `URLRequest`.
308 | static func == (mock: Mock, request: URLRequest) -> Bool {
309 | guard let requestHTTPMethod = Mock.HTTPMethod(rawValue: request.httpMethod ?? "") else { return false }
310 |
311 | if let fileExtensions = mock.fileExtensions {
312 | // If the mock contains a file extension, this should always be used to match for.
313 | guard let pathExtension = request.url?.pathExtension else { return false }
314 | return fileExtensions.contains(pathExtension)
315 | } else if mock.ignoreQuery {
316 | return mock.request.url!.baseString == request.url?.baseString && mock.data.keys.contains(requestHTTPMethod)
317 | }
318 |
319 | return mock.request.url!.absoluteString == request.url?.absoluteString && mock.data.keys.contains(requestHTTPMethod)
320 | }
321 |
322 | public static func == (lhs: Mock, rhs: Mock) -> Bool {
323 | let lhsHTTPMethods: [String] = lhs.data.keys.compactMap { $0.rawValue }.sorted()
324 | let rhsHTTPMethods: [String] = rhs.data.keys.compactMap { $0.rawValue }.sorted()
325 |
326 | if let lhsFileExtensions = lhs.fileExtensions, let rhsFileExtensions = rhs.fileExtensions, (!lhsFileExtensions.isEmpty || !rhsFileExtensions.isEmpty) {
327 | /// The mocks are targeting file extensions specifically, check on those.
328 | return lhsFileExtensions == rhsFileExtensions && lhsHTTPMethods == rhsHTTPMethods
329 | }
330 |
331 | return lhs.request.url!.absoluteString == rhs.request.url!.absoluteString && lhsHTTPMethods == rhsHTTPMethods
332 | }
333 | }
334 |
--------------------------------------------------------------------------------
/Sources/Mocker/Mocker.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Mocker.swift
3 | // Rabbit
4 | //
5 | // Created by Antoine van der Lee on 04/05/2017.
6 | // Copyright © 2017 WeTransfer. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | #if canImport(FoundationNetworking)
11 | import FoundationNetworking
12 | #endif
13 |
14 | /// Can be used for registering Mocked data, returned by the `MockingURLProtocol`.
15 | public struct Mocker {
16 | private struct IgnoredRule: Equatable {
17 | let urlToIgnore: URL
18 | let matchType: URLMatchType
19 |
20 | /// Checks if the passed URL should be ignored.
21 | ///
22 | /// - Parameter url: The URL to check for.
23 | /// - Returns: `true` if it should be ignored, `false` if the URL doesn't correspond to ignored rules.
24 | func shouldIgnore(_ url: URL) -> Bool {
25 | url.matches(urlToIgnore, matchType: matchType)
26 | }
27 | }
28 |
29 | public enum HTTPVersion: String {
30 | case http1_0 = "HTTP/1.0"
31 | case http1_1 = "HTTP/1.1"
32 | case http2_0 = "HTTP/2.0"
33 | }
34 |
35 | /// The way Mocker handles unregistered urls
36 | public enum Mode {
37 | /// The default mode: only URLs registered with the `ignore(_ url: URL)` method are ignored for mocking.
38 | ///
39 | /// - Registered mocked URL: Mocked.
40 | /// - Registered ignored URL: Ignored by Mocker, default process is applied as if the Mocker doesn't exist.
41 | /// - Any other URL: Raises an error.
42 | case optout
43 |
44 | /// Only registered mocked URLs are mocked, all others pass through.
45 | ///
46 | /// - Registered mocked URL: Mocked.
47 | /// - Any other URL: Ignored by Mocker, default process is applied as if the Mocker doesn't exist.
48 | case optin
49 | }
50 |
51 | /// The mode defines how unknown URLs are handled. Defaults to `optout` which means requests without a mock will fail.
52 | public static var mode: Mode = .optout
53 |
54 | /// The shared instance of the Mocker, can be used to register and return mocks.
55 | internal static var shared = Mocker()
56 |
57 | /// The HTTP Version to use in the mocked response.
58 | public static var httpVersion: HTTPVersion = HTTPVersion.http1_1
59 |
60 | /// The registrated mocks.
61 | private(set) var mocks: [Mock] = []
62 |
63 | /// URLs to ignore for mocking.
64 | public var ignoredURLs: [URL] {
65 | ignoredRules.map { $0.urlToIgnore }
66 | }
67 |
68 | private var ignoredRules: [IgnoredRule] = []
69 |
70 | /// For Thread Safety access.
71 | private let queue = DispatchQueue(label: "mocker.mocks.access.queue", attributes: .concurrent)
72 |
73 | private init() {
74 | // Whenever someone is requesting the Mocker, we want the URL protocol to be activated.
75 | _ = URLProtocol.registerClass(MockingURLProtocol.self)
76 | }
77 |
78 | /// Register new Mocked data. If a mock for the same URL and HTTPMethod exists, it will be overwritten.
79 | ///
80 | /// - Parameter mock: The Mock to be registered for future requests.
81 | public static func register(_ mock: Mock) {
82 | shared.queue.async(flags: .barrier) {
83 | /// Delete the Mock if it was already registered.
84 | shared.mocks.removeAll(where: { $0 == mock })
85 | shared.mocks.append(mock)
86 | }
87 | }
88 |
89 | /// Register an URL to ignore for mocking. This will let the URL work as if the Mocker doesn't exist.
90 | ///
91 | /// - Parameter url: The URL to ignore.
92 | /// - Parameter ignoreQuery: If `true`, checking the URL will ignore the query and match only for the scheme, host and path. Defaults to `false`.
93 | @available(*, deprecated, renamed: "ignore(_:matchType:)")
94 | public static func ignore(_ url: URL, ignoreQuery: Bool) {
95 | shared.queue.async(flags: .barrier) {
96 | let rule = IgnoredRule(urlToIgnore: url, matchType: ignoreQuery ? .ignoreQuery : .full)
97 | shared.ignoredRules.append(rule)
98 | }
99 | }
100 |
101 | /// Register an URL to ignore for mocking. This will let the URL work as if the Mocker doesn't exist.
102 | ///
103 | /// - Parameter url: The URL to ignore.
104 | /// - Parameter matchType: The approach that will be used to determine whether URLs match the provided URL. Defaults to `full`.
105 | public static func ignore(_ url: URL, matchType: URLMatchType = .full) {
106 | shared.queue.async(flags: .barrier) {
107 | let rule = IgnoredRule(urlToIgnore: url, matchType: matchType)
108 | shared.ignoredRules.append(rule)
109 | }
110 | }
111 |
112 | /// Checks if the passed URL should be handled by the Mocker. If the URL is registered to be ignored, it will not handle the URL.
113 | ///
114 | /// - Parameter url: The URL to check for.
115 | /// - Returns: `true` if it should be mocked, `false` if the URL is registered as ignored.
116 | public static func shouldHandle(_ request: URLRequest) -> Bool {
117 | switch mode {
118 | case .optout:
119 | guard let url = request.url else { return false }
120 | return shared.queue.sync {
121 | !shared.ignoredRules.contains(where: { $0.shouldIgnore(url) })
122 | }
123 | case .optin:
124 | return mock(for: request) != nil
125 | }
126 | }
127 |
128 | /// Removes all registered mocks. Use this method in your tearDown function to make sure a Mock is not used in any other test.
129 | public static func removeAll() {
130 | shared.queue.sync(flags: .barrier) {
131 | shared.mocks.removeAll()
132 | shared.ignoredRules.removeAll()
133 | }
134 | }
135 |
136 | /// Retrieve a Mock for the given request. Matches on `request.url` and `request.httpMethod`.
137 | ///
138 | /// - Parameter request: The request to search for a mock.
139 | /// - Returns: A mock if found, `nil` if there's no mocked data registered for the given request.
140 | static func mock(for request: URLRequest) -> Mock? {
141 | shared.queue.sync {
142 | /// First check for specific URLs
143 | if let specificMock = shared.mocks.first(where: { $0 == request && $0.fileExtensions == nil }) {
144 | return specificMock
145 | }
146 | /// Second, check for generic file extension Mocks
147 | return shared.mocks.first(where: { $0 == request })
148 | }
149 | }
150 | }
151 |
--------------------------------------------------------------------------------
/Sources/Mocker/MockingURLProtocol.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MockingURLProtocol.swift
3 | // Rabbit
4 | //
5 | // Created by Antoine van der Lee on 04/05/2017.
6 | // Copyright © 2017 WeTransfer. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | #if canImport(FoundationNetworking)
11 | import FoundationNetworking
12 | #endif
13 |
14 | /// The protocol which can be used to send Mocked data back. Use the `Mocker` to register `Mock` data
15 | open class MockingURLProtocol: URLProtocol {
16 |
17 | enum Error: Swift.Error, LocalizedError, CustomDebugStringConvertible {
18 | case missingMockedData(url: String)
19 | case explicitMockFailure(url: String)
20 |
21 | var errorDescription: String? {
22 | return debugDescription
23 | }
24 |
25 | var debugDescription: String {
26 | switch self {
27 | case .missingMockedData(let url):
28 | return "Missing mock for URL: \(url)"
29 | case .explicitMockFailure(url: let url):
30 | return "Induced error for URL: \(url)"
31 | }
32 | }
33 | }
34 |
35 | private var responseWorkItem: DispatchWorkItem?
36 |
37 | /// 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.
38 | override public func startLoading() {
39 | guard
40 | let mock = Mocker.mock(for: request),
41 | let response = HTTPURLResponse(url: mock.request.url!, statusCode: mock.statusCode, httpVersion: Mocker.httpVersion.rawValue, headerFields: mock.headers),
42 | let data = mock.data(for: request)
43 | else {
44 | 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")
45 | client?.urlProtocol(self, didFailWithError: Error.missingMockedData(url: String(describing: request.url?.absoluteString)))
46 | return
47 | }
48 |
49 | if let onRequestHandler = mock.onRequestHandler {
50 | onRequestHandler.handleRequest(request)
51 | }
52 | mock.onRequestExpectation?.fulfill()
53 |
54 | guard let delay = mock.delay else {
55 | finishRequest(for: mock, data: data, response: response)
56 | return
57 | }
58 |
59 | self.responseWorkItem = DispatchWorkItem(block: { [weak self] in
60 | guard let self = self else { return }
61 | self.finishRequest(for: mock, data: data, response: response)
62 | })
63 |
64 | DispatchQueue.global(qos: DispatchQoS.QoSClass.userInitiated).asyncAfter(deadline: .now() + delay, execute: responseWorkItem!)
65 | }
66 |
67 | private func finishRequest(for mock: Mock, data: Data, response: HTTPURLResponse) {
68 | if let redirectLocation = data.redirectLocation {
69 | self.client?.urlProtocol(self, wasRedirectedTo: URLRequest(url: redirectLocation), redirectResponse: response)
70 | } else if let requestError = mock.requestError {
71 | self.client?.urlProtocol(self, didFailWithError: requestError)
72 | } else {
73 | self.client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: mock.cacheStoragePolicy)
74 | self.client?.urlProtocol(self, didLoad: data)
75 | self.client?.urlProtocolDidFinishLoading(self)
76 | }
77 |
78 | mock.completion?()
79 | mock.onCompletedExpectation?.fulfill()
80 | }
81 |
82 | /// Implementation does nothing, but is needed for a valid inheritance of URLProtocol.
83 | override public func stopLoading() {
84 | responseWorkItem?.cancel()
85 | }
86 |
87 | /// Simply sends back the passed request. Implementation is needed for a valid inheritance of URLProtocol.
88 | override public class func canonicalRequest(for request: URLRequest) -> URLRequest {
89 | return request
90 | }
91 |
92 | /// Overrides needed to define a valid inheritance of URLProtocol.
93 | override public class func canInit(with request: URLRequest) -> Bool {
94 | return Mocker.shouldHandle(request)
95 | }
96 | }
97 |
98 | private extension Data {
99 | /// Returns the redirect location from the raw HTTP response if exists.
100 | var redirectLocation: URL? {
101 | let locationComponent = String(data: self, encoding: String.Encoding.utf8)?.components(separatedBy: "\n").first(where: { (value) -> Bool in
102 | return value.contains("Location:")
103 | })
104 |
105 | guard let redirectLocationString = locationComponent?.components(separatedBy: "Location:").last, let redirectLocation = URL(string: redirectLocationString.trimmingCharacters(in: NSCharacterSet.whitespaces)) else {
106 | return nil
107 | }
108 | return redirectLocation
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/Sources/Mocker/OnRequestHandler.swift:
--------------------------------------------------------------------------------
1 | //
2 | // OnRequestHandler.swift
3 | //
4 | //
5 | // Created by Antoine van der Lee on 03/11/2022.
6 | // Copyright © 2022 WeTransfer. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | #if canImport(FoundationNetworking)
11 | import FoundationNetworking
12 | #endif
13 |
14 | /// A handler for verifying outgoing requests.
15 | public struct OnRequestHandler {
16 |
17 | public typealias OnRequest = (_ request: URLRequest, _ httpBody: HTTPBody?) -> Void
18 |
19 | private let internalCallback: (_ request: URLRequest) -> Void
20 | let legacyCallback: Mock.OnRequest?
21 |
22 | /// Creates a new request handler using the given `HTTPBody` type, which can be any `Decodable`.
23 | /// - Parameters:
24 | /// - httpBodyType: The decodable type to use for parsing the request body.
25 | /// - callback: The callback which will be called just before the request executes.
26 | public init(httpBodyType: HTTPBody.Type?, callback: @escaping OnRequest) {
27 | self.init(httpBodyType: httpBodyType, jsonDecoder: JSONDecoder(), callback: callback)
28 | }
29 |
30 | /// Creates a new request handler using the given `HTTPBody` type, which can be any `Decodable` and decoding it using the provided `JSONDecoder`.
31 | /// - Parameters:
32 | /// - httpBodyType: The decodable type to use for parsing the request body.
33 | /// - jsonDecoder: The decoder to use for decoding the request body.
34 | /// - callback: The callback which will be called just before the request executes.
35 | public init(httpBodyType: HTTPBody.Type?, jsonDecoder: JSONDecoder, callback: @escaping OnRequest) {
36 | self.internalCallback = { request in
37 | guard
38 | let httpBody = request.httpBodyStreamData() ?? request.httpBody,
39 | let decodedObject = try? jsonDecoder.decode(HTTPBody.self, from: httpBody)
40 | else {
41 | callback(request, nil)
42 | return
43 | }
44 | callback(request, decodedObject)
45 | }
46 | legacyCallback = nil
47 | }
48 |
49 | /// Creates a new request handler using the given callback to call on request without parsing the body arguments.
50 | /// - Parameter requestCallback: The callback which will be executed just before the request executes, containing the request.
51 | public init(requestCallback: @escaping (_ request: URLRequest) -> Void) {
52 | self.internalCallback = requestCallback
53 | legacyCallback = nil
54 | }
55 |
56 | /// Creates a new request handler using the given callback to call on request without parsing the body arguments and without passing the request.
57 | /// - Parameter callback: The callback which will be executed just before the request executes.
58 | public init(callback: @escaping () -> Void) {
59 | self.internalCallback = { _ in
60 | callback()
61 | }
62 | legacyCallback = nil
63 | }
64 |
65 | /// Creates a new request handler using the given callback to call on request.
66 | /// - Parameter jsonDictionaryCallback: The callback that executes just before the request executes, containing the HTTP Body Arguments as a JSON Object Dictionary.
67 | public init(jsonDictionaryCallback: @escaping ((_ request: URLRequest, _ httpBodyArguments: [String: Any]?) -> Void)) {
68 | self.internalCallback = { request in
69 | guard
70 | let httpBody = request.httpBodyStreamData() ?? request.httpBody,
71 | let jsonObject = try? JSONSerialization.jsonObject(with: httpBody, options: .fragmentsAllowed) as? [String: Any]
72 | else {
73 | jsonDictionaryCallback(request, nil)
74 | return
75 | }
76 | jsonDictionaryCallback(request, jsonObject)
77 | }
78 | self.legacyCallback = nil
79 | }
80 |
81 | /// Creates a new request handler using the given callback to call on request.
82 | /// - Parameter jsonDictionaryCallback: The callback that executes just before the request executes, containing the HTTP Body Arguments as a JSON Object Array.
83 | public init(jsonArrayCallback: @escaping ((_ request: URLRequest, _ httpBodyArguments: [[String: Any]]?) -> Void)) {
84 | self.internalCallback = { request in
85 | guard
86 | let httpBody = request.httpBodyStreamData() ?? request.httpBody,
87 | let jsonObject = try? JSONSerialization.jsonObject(with: httpBody, options: .fragmentsAllowed) as? [[String: Any]]
88 | else {
89 | jsonArrayCallback(request, nil)
90 | return
91 | }
92 | jsonArrayCallback(request, jsonObject)
93 | }
94 | self.legacyCallback = nil
95 | }
96 |
97 | init(legacyCallback: Mock.OnRequest?) {
98 | self.internalCallback = { request in
99 | guard
100 | let httpBody = request.httpBodyStreamData() ?? request.httpBody,
101 | let jsonObject = try? JSONSerialization.jsonObject(with: httpBody, options: .fragmentsAllowed) as? [String: Any]
102 | else {
103 | legacyCallback?(request, nil)
104 | return
105 | }
106 | legacyCallback?(request, jsonObject)
107 | }
108 | self.legacyCallback = legacyCallback
109 | }
110 |
111 | func handleRequest(_ request: URLRequest) {
112 | internalCallback(request)
113 | }
114 | }
115 |
116 | private extension URLRequest {
117 | /// We need to use the http body stream data as the URLRequest once launched converts the `httpBody` to this stream of data.
118 | func httpBodyStreamData() -> Data? {
119 | guard let bodyStream = self.httpBodyStream else { return nil }
120 |
121 | bodyStream.open()
122 |
123 | // Will read 16 chars per iteration. Can use bigger buffer if needed
124 | let bufferSize: Int = 16
125 | let buffer = UnsafeMutablePointer.allocate(capacity: bufferSize)
126 | var data = Data()
127 |
128 | while bodyStream.hasBytesAvailable {
129 | let readData = bodyStream.read(buffer, maxLength: bufferSize)
130 | data.append(buffer, count: readData)
131 | }
132 |
133 | buffer.deallocate()
134 | bodyStream.close()
135 |
136 | return data
137 | }
138 | }
139 |
--------------------------------------------------------------------------------
/Sources/Mocker/URLMatchType.swift:
--------------------------------------------------------------------------------
1 | //
2 | // URLMatchType.swift
3 | // Mocker
4 | //
5 | // Created by Brent Whitman on 2024-04-18.
6 | //
7 |
8 | import Foundation
9 |
10 | /// How to check if one URL matches another.
11 | public enum URLMatchType {
12 | /// Matches the full URL, including the query
13 | case full
14 | /// Matches the URL excluding the query
15 | case ignoreQuery
16 | /// Matches if the URL begins with the prefix
17 | case prefix
18 | }
19 |
20 | extension URL {
21 | /// Returns the base URL string build with the scheme, host and path. "https://www.wetransfer.com/v1/test?param=test" would be "https://www.wetransfer.com/v1/test".
22 | var baseString: String? {
23 | guard let scheme = scheme, let host = host else { return nil }
24 | return scheme + "://" + host + path
25 | }
26 |
27 | /// Checks if this URL matches the passed URL using the provided match type.
28 | ///
29 | /// - Parameter url: The URL to check for a match.
30 | /// - Parameter matchType: The approach that will be used to determine whether this URL match the provided URL. Defaults to `full`.
31 | /// - Returns: `true` if the URL matches based on the match type; `false` otherwise.
32 | func matches(_ otherURL: URL?, matchType: URLMatchType = .full) -> Bool {
33 | guard let otherURL else { return false }
34 |
35 | switch matchType {
36 | case .full:
37 | return absoluteString == otherURL.absoluteString
38 | case .ignoreQuery:
39 | return baseString == otherURL.baseString
40 | case .prefix:
41 | return absoluteString.hasPrefix(otherURL.absoluteString)
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/Sources/Mocker/XCTest+Mocker.swift:
--------------------------------------------------------------------------------
1 | //
2 | // XCTest+Mocker.swift
3 | // Mocker
4 | //
5 | // Created by Antoine van der Lee on 27/05/2020.
6 | // Copyright © 2020 WeTransfer. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import XCTest
11 |
12 | public extension XCTestCase {
13 | func expectationForRequestingMock(_ mock: inout Mock) -> XCTestExpectation {
14 | let mockExpectation = expectation(description: "\(mock) should be requested")
15 | mock.onRequestExpectation = mockExpectation
16 | return mockExpectation
17 | }
18 |
19 | func expectationForCompletingMock(_ mock: inout Mock) -> XCTestExpectation {
20 | let mockExpectation = expectation(description: "\(mock) should be finishing")
21 | mock.onCompletedExpectation = mockExpectation
22 | return mockExpectation
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/Tests/MockerTests/MockTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MockTests.swift
3 | //
4 | //
5 | // Created by Antoine van der Lee on 21/04/2021.
6 | //
7 |
8 | import Foundation
9 | import XCTest
10 | @testable import Mocker
11 |
12 | final class MockTests: XCTestCase {
13 | override func setUp() {
14 | super.setUp()
15 | Mocker.mode = .optout
16 | }
17 |
18 | override func tearDown() {
19 | Mocker.removeAll()
20 | Mocker.mode = .optout
21 | super.tearDown()
22 | }
23 |
24 | /// It should match two file extension mocks correctly.
25 | func testFileExtensionMocksComparing() {
26 | let mock200 = Mock(fileExtensions: "png", contentType: .imagePNG, statusCode: 200, data: [.put: Data()])
27 | let secondMock200 = Mock(fileExtensions: "png", contentType: .imagePNG, statusCode: 200, data: [.put: Data()])
28 | let mock400 = Mock(fileExtensions: "png", contentType: .imagePNG, statusCode: 400, data: [.put: Data()])
29 | let mockJPEG = Mock(fileExtensions: "jpeg", contentType: .imagePNG, statusCode: 200, data: [.put: Data()])
30 |
31 | XCTAssertEqual(mock200, secondMock200)
32 | XCTAssertEqual(mock200, mock400)
33 | XCTAssertNotEqual(mock200, mockJPEG)
34 | }
35 |
36 | func testMethodsComparing() {
37 | let url = URL(string: "https://mocked.wetransfer.com")!
38 |
39 | let methods = [Mock.HTTPMethod.options, .get, .head, .post, .put, .patch, .delete, .trace, .connect]
40 | let first = Mock(url: url, statusCode: 200, data: Dictionary(uniqueKeysWithValues: methods.shuffled().map { ($0, Data()) }))
41 | let second = Mock(url: url, statusCode: 200, data: Dictionary(uniqueKeysWithValues: methods.shuffled().map { ($0, Data()) }))
42 | XCTAssertEqual(first, second)
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/Tests/MockerTests/MockedData.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MockedData.swift
3 | // Mocker
4 | //
5 | // Created by Antoine van der Lee on 11/08/2017.
6 | // Copyright © 2017 WeTransfer. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | /// Contains all available Mocked data.
12 | public final class MockedData {
13 | public static let botAvatarImageFileUrl: URL = Bundle.module.url(forResource: "wetransfer_bot_avatar", withExtension: "png")!
14 | public static let exampleJSON: URL = Bundle.module.url(forResource: "example", withExtension: "json")!
15 | public static let redirectGET: URL = Bundle.module.url(forResource: "sample-redirect-get", withExtension: "data")!
16 | }
17 |
18 | extension Bundle {
19 | #if !SWIFT_PACKAGE
20 | static let module = Bundle(for: MockedData.self)
21 | #endif
22 | }
23 |
24 | internal extension URL {
25 | /// Returns a `Data` representation of the current `URL`. Force unwrapping as it's only used for tests.
26 | var data: Data {
27 | return try! Data(contentsOf: self)
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/Tests/MockerTests/MockerTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MockerTests.swift
3 | // MockerTests
4 | //
5 | // Created by Antoine van der Lee on 11/08/2017.
6 | // Copyright © 2017 WeTransfer. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | #if canImport(FoundationNetworking)
11 | import FoundationNetworking
12 | #endif
13 | @testable import Mocker
14 |
15 | final class MockerTests: XCTestCase {
16 | struct Framework {
17 | let name: String?
18 | let owner: String?
19 |
20 | init(jsonDictionary: [String: Any]) {
21 | name = jsonDictionary["name"] as? String
22 | owner = jsonDictionary["owner"] as? String
23 | }
24 | }
25 |
26 | override func setUp() {
27 | super.setUp()
28 | Mocker.mode = .optout
29 | }
30 |
31 | override func tearDown() {
32 | Mocker.removeAll()
33 | Mocker.mode = .optout
34 | super.tearDown()
35 | }
36 |
37 | /// It should returned the register mocked image data as response.
38 | func testImageURLDataRequest() {
39 | let expectation = self.expectation(description: "Data request should succeed")
40 | let originalURL = URL(string: "https://avatars3.githubusercontent.com/u/26250426?v=4&s=400")!
41 |
42 | let mockedData = MockedData.botAvatarImageFileUrl.data
43 | let mock = Mock(url: originalURL, contentType: .imagePNG, statusCode: 200, data: [
44 | .get: mockedData
45 | ])
46 |
47 | mock.register()
48 | URLSession.shared.dataTask(with: originalURL) { (data, _, error) in
49 | XCTAssertNil(error)
50 | XCTAssertEqual(data, mockedData, "Image should be returned mocked")
51 | expectation.fulfill()
52 | }.resume()
53 |
54 | waitForExpectations(timeout: 10.0, handler: nil)
55 | }
56 |
57 | /// It should returned the register mocked image data as response for register file types.
58 | func testImageExtensionDataRequest() {
59 | let expectation = self.expectation(description: "Data request should succeed")
60 | let originalURL = URL(string: "https://www.wetransfer.com/sample-image.png")
61 |
62 | let mockedData = MockedData.botAvatarImageFileUrl.data
63 | Mock(fileExtensions: "png", contentType: .imagePNG, statusCode: 200, data: [
64 | .get: mockedData
65 | ]).register()
66 |
67 | URLSession.shared.dataTask(with: originalURL!) { (data, _, error) in
68 | XCTAssertNil(error)
69 | XCTAssertEqual(data, mockedData, "Image should be returned mocked")
70 | expectation.fulfill()
71 | }.resume()
72 |
73 | waitForExpectations(timeout: 10.0, handler: nil)
74 | }
75 |
76 | /// It should ignore file extension mocks if a specific URL is mocked.
77 | func testSpecificURLOverGenericMocks() {
78 | let expectation = self.expectation(description: "Data request should succeed")
79 | let originalURL = URL(string: "https://www.wetransfer.com/sample-image.png")!
80 |
81 | Mock(fileExtensions: "png", contentType: .imagePNG, statusCode: 400, data: [
82 | .get: Data()
83 | ]).register()
84 |
85 | let mockedData = MockedData.botAvatarImageFileUrl.data
86 | Mock(url: originalURL, ignoreQuery: true, contentType: .imagePNG, statusCode: 200, data: [
87 | .get: mockedData
88 | ]).register()
89 |
90 | URLSession.shared.dataTask(with: originalURL) { (data, _, error) in
91 | XCTAssertNil(error)
92 | XCTAssertEqual(data, mockedData, "Image should be returned mocked")
93 | expectation.fulfill()
94 | }.resume()
95 |
96 | waitForExpectations(timeout: 10.0, handler: nil)
97 | }
98 |
99 | /// It should correctly ignore queries if set.
100 | func testIgnoreQueryMocking() {
101 | let expectation = self.expectation(description: "Data request should succeed")
102 | let originalURL = URL(string: "https://www.wetransfer.com/sample-image.png?width=200&height=200")!
103 |
104 | let mockedData = MockedData.botAvatarImageFileUrl.data
105 | Mock(url: originalURL, ignoreQuery: true, contentType: .imagePNG, statusCode: 200, data: [
106 | .get: mockedData
107 | ]).register()
108 |
109 | /// Make it different compared to the mocked URL.
110 | let customURL = URL(string: originalURL.absoluteString + "&" + UUID().uuidString)!
111 |
112 | URLSession.shared.dataTask(with: customURL) { (data, _, error) in
113 | XCTAssertNil(error)
114 | XCTAssertEqual(data, mockedData, "Image should be returned mocked")
115 | expectation.fulfill()
116 | }.resume()
117 |
118 | waitForExpectations(timeout: 10.0, handler: nil)
119 | }
120 |
121 | /// It should return the mocked JSON.
122 | func testJSONRequest() {
123 | let expectation = self.expectation(description: "Data request should succeed")
124 | let originalURL = URL(string: "https://www.wetransfer.com/example.json")!
125 |
126 | Mock(url: originalURL, contentType: .json, statusCode: 200, data: [
127 | .get: MockedData.exampleJSON.data
128 | ]).register()
129 |
130 | URLSession.shared.dataTask(with: originalURL) { (data, _, _) in
131 |
132 | guard let data = data else {
133 | XCTFail("Data is nil")
134 | return
135 | }
136 |
137 | guard let jsonDictionary = (try? JSONSerialization.jsonObject(with: data)) as? [String: Any] else {
138 | XCTFail("Wrong data response \(String(describing: data))")
139 | expectation.fulfill()
140 | return
141 | }
142 |
143 | let framework = Framework(jsonDictionary: jsonDictionary)
144 | XCTAssertEqual(framework.name, "Mocker")
145 | XCTAssertEqual(framework.owner, "WeTransfer")
146 |
147 | expectation.fulfill()
148 | }.resume()
149 |
150 | waitForExpectations(timeout: 10.0, handler: nil)
151 | }
152 |
153 | /// No Content-Type should be included in the headers
154 | func testNoContentType() {
155 | let expectation = self.expectation(description: "Data request should succeed")
156 | let originalURL = URL(string: "https://www.wetransfer.com/api/foobar")!
157 | var request = URLRequest(url: originalURL)
158 | request.httpMethod = "PUT"
159 |
160 | Mock(request: request, statusCode: 202).register()
161 |
162 | URLSession.shared.dataTask(with: request) { (data, response, _) in
163 | guard let response = response as? HTTPURLResponse else {
164 | XCTFail("Unexpected response")
165 | return
166 | }
167 |
168 | // data is only nil if there is an error
169 | XCTAssertEqual(data, Data())
170 | XCTAssertNil(response.allHeaderFields["Content-Type"])
171 |
172 | expectation.fulfill()
173 | }.resume()
174 |
175 | waitForExpectations(timeout: 10.0, handler: nil)
176 | }
177 |
178 | /// It should return the additional headers.
179 | func testAdditionalHeaders() {
180 | let expectation = self.expectation(description: "Data request should succeed")
181 | let headers = ["Testkey": "testvalue"]
182 | let mock = Mock(contentType: .json, statusCode: 200, data: [.get: Data()], additionalHeaders: headers)
183 | mock.register()
184 |
185 | URLSession.shared.dataTask(with: mock.request) { (_, response, error) in
186 | XCTAssertNil(error)
187 | XCTAssertEqual(((response as? HTTPURLResponse)?.allHeaderFields["Testkey"] as? String), "testvalue", "Additional headers should be added.")
188 | expectation.fulfill()
189 | }.resume()
190 |
191 | waitForExpectations(timeout: 10.0, handler: nil)
192 | }
193 |
194 | /// It should override existing mocks.
195 | func testMockOverriding() {
196 | let expectation = self.expectation(description: "Data request should succeed")
197 | let mock = Mock(contentType: .json, statusCode: 200, data: [.get: Data()], additionalHeaders: ["testkey": "testvalue"])
198 | mock.register()
199 |
200 | let newMock = Mock(contentType: .json, statusCode: 200, data: [.get: Data()], additionalHeaders: ["Newkey": "newvalue"])
201 | newMock.register()
202 |
203 | URLSession.shared.dataTask(with: mock.request) { (_, response, error) in
204 | XCTAssertNil(error)
205 | XCTAssertEqual(((response as? HTTPURLResponse)?.allHeaderFields["Newkey"] as? String), "newvalue", "Additional headers should be added.")
206 | expectation.fulfill()
207 | }.resume()
208 |
209 | waitForExpectations(timeout: 10.0, handler: nil)
210 | }
211 |
212 | /// It should work with a custom URLSession.
213 | func testCustomURLSession() {
214 | let expectation = self.expectation(description: "Data request should succeed")
215 | let originalURL = URL(string: "https://www.wetransfer.com/sample-image.png")
216 |
217 | let mockedData = MockedData.botAvatarImageFileUrl.data
218 | Mock(fileExtensions: "png", contentType: .imagePNG, statusCode: 200, data: [
219 | .get: mockedData
220 | ]).register()
221 |
222 | let configuration = URLSessionConfiguration.default
223 | configuration.protocolClasses = [MockingURLProtocol.self]
224 | let urlSession = URLSession(configuration: configuration)
225 |
226 | urlSession.dataTask(with: originalURL!) { (data, _, error) in
227 | XCTAssertNil(error)
228 | XCTAssertEqual(data, mockedData, "Image should be returned mocked")
229 | expectation.fulfill()
230 | }.resume()
231 |
232 | waitForExpectations(timeout: 10.0, handler: nil)
233 | }
234 |
235 | /// It should be possible to test cancellation of requests with a delayed mock.
236 | func testDelayedMockCancelation() {
237 | let expectation = self.expectation(description: "Data request should be cancelled")
238 | var mock = Mock(contentType: .json, statusCode: 200, data: [.get: Data()])
239 | mock.delay = DispatchTimeInterval.seconds(5)
240 | mock.register()
241 |
242 | let task = URLSession.shared.dataTask(with: mock.request) { (_, _, error) in
243 | XCTAssertEqual(error?._code, NSURLErrorCancelled)
244 | expectation.fulfill()
245 | }
246 |
247 | task.resume()
248 |
249 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.5, execute: {
250 | task.cancel()
251 | })
252 | waitForExpectations(timeout: 10.0, handler: nil)
253 | }
254 |
255 | /// It should correctly handle redirect responses.
256 | func testRedirectResponse() throws {
257 | #if os(Linux)
258 | throw XCTSkip("The URLSession swift-corelibs-foundation implementation doesn't currently handle redirects directly")
259 | #endif
260 | let expectation = self.expectation(description: "Data request should be cancelled")
261 | let urlWhichRedirects: URL = URL(string: "https://we.tl/redirect")!
262 | Mock(url: urlWhichRedirects, contentType: .html, statusCode: 200, data: [.get: MockedData.redirectGET.data]).register()
263 | Mock(url: URL(string: "https://wetransfer.com/redirect")!, contentType: .json, statusCode: 200, data: [.get: MockedData.exampleJSON.data]).register()
264 |
265 | URLSession.shared.dataTask(with: urlWhichRedirects) { (data, _, _) in
266 |
267 | guard let data = data else {
268 | XCTFail("Data is nil")
269 | return
270 | }
271 |
272 | guard let jsonDictionary = (try? JSONSerialization.jsonObject(with: data)) as? [String: Any] else {
273 | XCTFail("Wrong data response \(String(describing: data))")
274 | expectation.fulfill()
275 | return
276 | }
277 |
278 | let framework = Framework(jsonDictionary: jsonDictionary)
279 | XCTAssertEqual(framework.name, "Mocker")
280 | XCTAssertEqual(framework.owner, "WeTransfer")
281 |
282 | expectation.fulfill()
283 | }.resume()
284 |
285 | waitForExpectations(timeout: 10.0, handler: nil)
286 | }
287 |
288 | /// It should be possible to ignore URLs and not let them be handled.
289 | func testIgnoreURLs() {
290 |
291 | let ignoredURL = URL(string: "www.wetransfer.com")!
292 |
293 | XCTAssertTrue(MockingURLProtocol.canInit(with: URLRequest(url: ignoredURL)))
294 | Mocker.ignore(ignoredURL)
295 | XCTAssertFalse(MockingURLProtocol.canInit(with: URLRequest(url: ignoredURL)))
296 | }
297 |
298 | /// It should be possible to ignore URLs and not let them be handled.
299 | func testIgnoreURLsIgnoreQueries() {
300 |
301 | let ignoredURL = URL(string: "https://www.wetransfer.com/sample-image.png")!
302 | let ignoredURLQueries = URL(string: "https://www.wetransfer.com/sample-image.png?width=200&height=200")!
303 |
304 | XCTAssertTrue(MockingURLProtocol.canInit(with: URLRequest(url: ignoredURLQueries)))
305 | Mocker.ignore(ignoredURL, matchType: .ignoreQuery)
306 | XCTAssertFalse(MockingURLProtocol.canInit(with: URLRequest(url: ignoredURLQueries)))
307 | }
308 |
309 | /// It should be possible to ignore URL prefixes and not let them be handled.
310 | func testIgnoreURLsIgnorePrefixes() {
311 |
312 | let ignoredURL = URL(string: "https://www.wetransfer.com/private")!
313 | let ignoredURLSubPath = URL(string: "https://www.wetransfer.com/private/sample-image.png")!
314 |
315 | XCTAssertTrue(MockingURLProtocol.canInit(with: URLRequest(url: ignoredURLSubPath)))
316 | Mocker.ignore(ignoredURL, matchType: .prefix)
317 | XCTAssertFalse(MockingURLProtocol.canInit(with: URLRequest(url: ignoredURLSubPath)))
318 | XCTAssertFalse(MockingURLProtocol.canInit(with: URLRequest(url: ignoredURL)))
319 | }
320 |
321 | /// It should be possible to compose a url relative to a base and still have it match the full url
322 | func testComposedURLMatch() {
323 | let composedURL = URL(fileURLWithPath: "resource", relativeTo: URL(string: "https://host.com/api/"))
324 | let simpleURL = URL(string: "https://host.com/api/resource")
325 | let mock = Mock(url: composedURL, contentType: .json, statusCode: 200, data: [.get: MockedData.exampleJSON.data])
326 | let urlRequest = URLRequest(url: simpleURL!)
327 | XCTAssertEqual(composedURL.absoluteString, simpleURL?.absoluteString)
328 | XCTAssert(mock == urlRequest)
329 | }
330 |
331 | /// It should call the onRequest and completion callbacks when a `Mock` is used and completed in the right order.
332 | func testMockCallbacks() {
333 | let onRequestExpectation = expectation(description: "Data request should start")
334 | let completionExpectation = expectation(description: "Data request should succeed")
335 | var mock = Mock(contentType: .json, statusCode: 200, data: [.get: Data()])
336 | mock.onRequest = { _, _ in
337 | onRequestExpectation.fulfill()
338 | }
339 | mock.completion = {
340 | completionExpectation.fulfill()
341 | }
342 | mock.register()
343 |
344 | URLSession.shared.dataTask(with: mock.request).resume()
345 |
346 | wait(for: [onRequestExpectation, completionExpectation], timeout: 2.0, enforceOrder: true)
347 | }
348 |
349 | /// It should report post body arguments if they exist.
350 | func testOnRequestLegacyPostBodyParameters() throws {
351 | let onRequestExpectation = expectation(description: "Data request should start")
352 |
353 | let expectedParameters = ["test": "value"]
354 | let requestURL = URL(string: "https://www.fakeurl.com")!
355 | var request = URLRequest(url: requestURL)
356 | request.httpMethod = Mock.HTTPMethod.post.rawValue
357 | request.httpBody = try JSONSerialization.data(withJSONObject: expectedParameters, options: .prettyPrinted)
358 |
359 | var mock = Mock(url: requestURL, contentType: .json, statusCode: 200, data: [.post: Data()])
360 | mock.onRequest = { request, postBodyArguments in
361 | XCTAssertEqual(request.url, requestURL)
362 | XCTAssertEqual(expectedParameters, postBodyArguments as? [String: String])
363 | onRequestExpectation.fulfill()
364 | }
365 | mock.register()
366 |
367 | URLSession.shared.dataTask(with: request).resume()
368 |
369 | wait(for: [onRequestExpectation], timeout: 2.0)
370 | }
371 |
372 | func testOnRequestDecodablePostBodyParameters() throws {
373 | struct RequestParameters: Codable, Equatable {
374 | let name: String
375 | }
376 |
377 | let onRequestExpectation = expectation(description: "Data request should start")
378 |
379 | let expectedParameters = RequestParameters(name: UUID().uuidString)
380 | let requestURL = URL(string: "https://www.fakeurl.com")!
381 | var request = URLRequest(url: requestURL)
382 | request.httpMethod = Mock.HTTPMethod.post.rawValue
383 | request.httpBody = try JSONEncoder().encode(expectedParameters)
384 |
385 | var mock = Mock(url: request.url!, contentType: .json, statusCode: 200, data: [.post: Data()])
386 | mock.onRequestHandler = .init(httpBodyType: RequestParameters.self, callback: { request, postBodyDecodable in
387 | XCTAssertEqual(request.url, requestURL)
388 | XCTAssertEqual(expectedParameters, postBodyDecodable)
389 | onRequestExpectation.fulfill()
390 | })
391 | mock.register()
392 |
393 | URLSession.shared.dataTask(with: request).resume()
394 |
395 | wait(for: [onRequestExpectation], timeout: 2.0)
396 | }
397 |
398 | func testOnRequestDecodablePostBodyParametersWithCustomJSONDecoder() throws {
399 | struct RequestParameters: Codable, Equatable {
400 | let name: String
401 | }
402 |
403 | let onRequestExpectation = expectation(description: "Data request should start")
404 |
405 | let expectedParameters = RequestParameters(name: UUID().uuidString)
406 | let requestURL = URL(string: "https://www.fakeurl.com")!
407 | var request = URLRequest(url: requestURL)
408 | request.httpMethod = Mock.HTTPMethod.post.rawValue
409 | request.httpBody = try JSONEncoder().encode(expectedParameters)
410 |
411 | var mock = Mock(url: request.url!, contentType: .json, statusCode: 200, data: [.post: Data()])
412 | mock.onRequestHandler = .init(httpBodyType: RequestParameters.self, jsonDecoder: JSONDecoder(), callback: { request, postBodyDecodable in
413 | XCTAssertEqual(request.url, requestURL)
414 | XCTAssertEqual(expectedParameters, postBodyDecodable)
415 | onRequestExpectation.fulfill()
416 | })
417 | mock.register()
418 |
419 | URLSession.shared.dataTask(with: request).resume()
420 |
421 | wait(for: [onRequestExpectation], timeout: 2.0)
422 | }
423 |
424 | func testOnRequestJSONDictionaryPostBodyParameters() throws {
425 | let onRequestExpectation = expectation(description: "Data request should start")
426 |
427 | let expectedParameters = ["test": "value"]
428 | let requestURL = URL(string: "https://www.fakeurl.com")!
429 | var request = URLRequest(url: requestURL)
430 | request.httpMethod = Mock.HTTPMethod.post.rawValue
431 | request.httpBody = try JSONSerialization.data(withJSONObject: expectedParameters, options: .prettyPrinted)
432 |
433 | var mock = Mock(url: request.url!, contentType: .json, statusCode: 200, data: [.post: Data()])
434 | mock.onRequestHandler = .init(jsonDictionaryCallback: { request, postBodyArguments in
435 | XCTAssertEqual(request.url, requestURL)
436 | XCTAssertEqual(expectedParameters, postBodyArguments as? [String: String])
437 | onRequestExpectation.fulfill()
438 | })
439 | mock.register()
440 |
441 | URLSession.shared.dataTask(with: request).resume()
442 |
443 | wait(for: [onRequestExpectation], timeout: 2.0)
444 | }
445 |
446 | func testOnRequestCallbackWithoutRequestAndParameters() throws {
447 | let onRequestExpectation = expectation(description: "Data request should start")
448 |
449 | var request = URLRequest(url: URL(string: "https://www.fakeurl.com")!)
450 | request.httpMethod = Mock.HTTPMethod.post.rawValue
451 |
452 | var mock = Mock(url: request.url!, contentType: .json, statusCode: 200, data: [.post: Data()])
453 | mock.onRequestHandler = .init(callback: {
454 | onRequestExpectation.fulfill()
455 | })
456 | mock.register()
457 |
458 | URLSession.shared.dataTask(with: request).resume()
459 |
460 | wait(for: [onRequestExpectation], timeout: 2.0)
461 | }
462 |
463 | /// It should report post body arguments with top level collection type if they exist.
464 | func testOnRequestPostBodyParametersWithTopLevelCollectionType() throws {
465 | let onRequestExpectation = expectation(description: "Data request should start")
466 |
467 | let expectedParameters = [["test": "value"], ["test": "value"]]
468 | let requestURL = URL(string: "https://www.fakeurl.com")!
469 | var request = URLRequest(url: requestURL)
470 | request.httpMethod = Mock.HTTPMethod.post.rawValue
471 | request.httpBody = try JSONSerialization.data(withJSONObject: expectedParameters, options: .prettyPrinted)
472 |
473 | var mock = Mock(url: request.url!, contentType: .json, statusCode: 200, data: [.post: Data()])
474 | mock.onRequestHandler = OnRequestHandler(jsonArrayCallback: { request, postBodyArguments in
475 | XCTAssertEqual(request.url, requestURL)
476 | XCTAssertEqual(expectedParameters, postBodyArguments as? [[String: String]])
477 | onRequestExpectation.fulfill()
478 | })
479 | mock.register()
480 |
481 | URLSession.shared.dataTask(with: request).resume()
482 |
483 | wait(for: [onRequestExpectation], timeout: 2.0)
484 | }
485 |
486 | /// It should call the mock after a delay.
487 | func testDelayedMock() {
488 | let nonDelayExpectation = expectation(description: "Data request should succeed")
489 | let delayedExpectation = expectation(description: "Data request should succeed")
490 | var delayedMock = Mock(contentType: .json, statusCode: 200, data: [.get: Data()])
491 | delayedMock.delay = DispatchTimeInterval.seconds(1)
492 | delayedMock.completion = {
493 | delayedExpectation.fulfill()
494 | }
495 | delayedMock.register()
496 | var nonDelayMock = Mock(contentType: .json, statusCode: 200, data: [.post: Data()])
497 | nonDelayMock.completion = {
498 | nonDelayExpectation.fulfill()
499 | }
500 | nonDelayMock.register()
501 |
502 | XCTAssertNotEqual(delayedMock.request.url, nonDelayMock.request.url)
503 |
504 | URLSession.shared.dataTask(with: delayedMock.request).resume()
505 | URLSession.shared.dataTask(with: nonDelayMock.request).resume()
506 |
507 | wait(for: [nonDelayExpectation, delayedExpectation], timeout: 2.0, enforceOrder: true)
508 | }
509 |
510 | /// It should remove all registered mocks correctly.
511 | func testRemoveAll() {
512 | let mock = Mock(contentType: .json, statusCode: 200, data: [.get: Data()])
513 | mock.register()
514 | Mocker.removeAll()
515 | XCTAssertTrue(Mocker.shared.mocks.isEmpty)
516 | }
517 |
518 | /// It should correctly add two mocks for the same URL if the HTTP method is different.
519 | func testDifferentHTTPMethodSameURL() {
520 | let url = URL(string: "https://www.fakeurl.com/\(UUID().uuidString)")!
521 | Mock(url: url, contentType: .json, statusCode: 200, data: [.get: Data()]).register()
522 | Mock(url: url, contentType: .json, statusCode: 200, data: [.put: Data()]).register()
523 | var request = URLRequest(url: url)
524 | request.httpMethod = Mock.HTTPMethod.get.rawValue
525 | XCTAssertNotNil(Mocker.mock(for: request))
526 | request.httpMethod = Mock.HTTPMethod.put.rawValue
527 | XCTAssertNotNil(Mocker.mock(for: request))
528 | }
529 |
530 | /// It should call the on request expectation.
531 | func testOnRequestExpectation() {
532 | let url = URL(string: "https://www.fakeurl.com")!
533 |
534 | var mock = Mock(url: url, contentType: .json, statusCode: 200, data: [.get: Data()])
535 | let expectation = expectationForRequestingMock(&mock)
536 | mock.register()
537 |
538 | URLSession.shared.dataTask(with: URLRequest(url: url)).resume()
539 |
540 | wait(for: [expectation], timeout: 2.0)
541 | }
542 |
543 | /// It should call the on completion expectation.
544 | func testOnCompletionExpectation() {
545 | let url = URL(string: "https://www.fakeurl.com")!
546 |
547 | var mock = Mock(url: url, contentType: .json, statusCode: 200, data: [.get: Data()])
548 | let expectation = expectationForCompletingMock(&mock)
549 | mock.register()
550 |
551 | URLSession.shared.dataTask(with: URLRequest(url: url)).resume()
552 |
553 | wait(for: [expectation], timeout: 2.0)
554 | }
555 |
556 | /// it should return the error we requested from the mock when we pass in an Error.
557 | func testMockReturningError() {
558 | let expectation = self.expectation(description: "Data request should succeed")
559 | let originalURL = URL(string: "https://www.wetransfer.com/example.json")!
560 |
561 | enum TestExampleError: Error, LocalizedError {
562 | case example
563 |
564 | var errorDescription: String { "example" }
565 | }
566 |
567 | Mock(url: originalURL, contentType: .json, statusCode: 500, data: [.get: Data()], requestError: TestExampleError.example).register()
568 |
569 | URLSession.shared.dataTask(with: originalURL) { (data, urlresponse, error) in
570 |
571 | XCTAssertNil(data)
572 | XCTAssertNil(urlresponse)
573 | XCTAssertNotNil(error)
574 | if let error = error {
575 | #if os(Linux)
576 | XCTAssertEqual(error as? TestExampleError, .example)
577 | #else
578 | // there's not a particularly elegant way to verify an instance
579 | // of an error, but this is a convenient workaround for testing
580 | // purposes
581 | XCTAssertTrue(String(describing: error).contains("TestExampleError"))
582 | #endif
583 | }
584 |
585 | expectation.fulfill()
586 | }.resume()
587 |
588 | waitForExpectations(timeout: 10.0, handler: nil)
589 | }
590 |
591 | /// It should cache response
592 | func testMockCachePolicy() throws {
593 | #if os(Linux)
594 | throw XCTSkip("URLSessionTask in swift-corelibs-foundation doesn't cache response for custom protocols")
595 | #endif
596 | let expectation = self.expectation(description: "Data request should succeed")
597 | let originalURL = URL(string: "https://www.wetransfer.com/example.json")!
598 |
599 | Mock(url: originalURL, cacheStoragePolicy: .allowed,
600 | contentType: .json, statusCode: 200,
601 | data: [.get: MockedData.exampleJSON.data],
602 | additionalHeaders: ["Cache-Control": "public, max-age=31557600, immutable"]
603 | ).register()
604 |
605 | let configuration = URLSessionConfiguration.default
606 | #if !os(Linux)
607 | configuration.urlCache = URLCache()
608 | #endif
609 | configuration.protocolClasses = [MockingURLProtocol.self]
610 | let urlSession = URLSession(configuration: configuration)
611 |
612 | urlSession.dataTask(with: originalURL) { (_, _, error) in
613 | XCTAssertNil(error)
614 |
615 | let cachedResponse = configuration.urlCache?.cachedResponse(for: URLRequest(url: originalURL))
616 | XCTAssertNotNil(cachedResponse)
617 | XCTAssertEqual(cachedResponse!.data, MockedData.exampleJSON.data)
618 |
619 | expectation.fulfill()
620 | }.resume()
621 |
622 | waitForExpectations(timeout: 10.0, handler: nil)
623 | }
624 |
625 | /// It should process unknown URL
626 | func testMockerOptoutMode() {
627 | Mocker.mode = .optout
628 |
629 | let mockedURL = URL(string: "www.google.com")!
630 | let ignoredURL = URL(string: "www.wetransfer.com")!
631 | let unknownURL = URL(string: "www.netflix.com")!
632 |
633 | // Mocking
634 | Mock(url: mockedURL, contentType: .json, statusCode: 200, data: [.get: Data()])
635 | .register()
636 |
637 | // Ignoring
638 | Mocker.ignore(ignoredURL)
639 |
640 | // Checking mocked URL are processed by Mocker
641 | XCTAssertTrue(MockingURLProtocol.canInit(with: URLRequest(url: mockedURL)))
642 | // Checking ignored URL are not processed by Mocker
643 | XCTAssertFalse(MockingURLProtocol.canInit(with: URLRequest(url: ignoredURL)))
644 |
645 | // Checking unknown URL are processed by Mocker (.optout mode)
646 | XCTAssertTrue(MockingURLProtocol.canInit(with: URLRequest(url: unknownURL)))
647 | }
648 |
649 | /// It should not process unknown URL
650 | func testMockerOptinMode() {
651 | Mocker.mode = .optin
652 |
653 | let mockedURL = URL(string: "www.google.com")!
654 | let ignoredURL = URL(string: "www.wetransfer.com")!
655 | let unknownURL = URL(string: "www.netflix.com")!
656 |
657 | // Mocking
658 | Mock(url: mockedURL, contentType: .json, statusCode: 200, data: [.get: Data()])
659 | .register()
660 |
661 | // Ignoring
662 | Mocker.ignore(ignoredURL)
663 |
664 | // Checking mocked URL are processed by Mocker
665 | XCTAssertTrue(MockingURLProtocol.canInit(with: URLRequest(url: mockedURL)))
666 | // Checking ignored URL are not processed by Mocker
667 | XCTAssertFalse(MockingURLProtocol.canInit(with: URLRequest(url: ignoredURL)))
668 |
669 | // Checking unknown URL are not processed by Mocker (.optin mode)
670 | XCTAssertFalse(MockingURLProtocol.canInit(with: URLRequest(url: unknownURL)))
671 | }
672 |
673 | /// Default mode should be .optout
674 | func testDefaultMode() {
675 | /// Checking default mode
676 | XCTAssertEqual(.optout, Mocker.mode)
677 | }
678 | }
679 |
--------------------------------------------------------------------------------
/Tests/MockerTests/Resources/JSON Files/example.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Mocker",
3 | "owner": "WeTransfer"
4 | }
5 |
--------------------------------------------------------------------------------
/Tests/MockerTests/Resources/sample-redirect-get.data:
--------------------------------------------------------------------------------
1 | HTTP/1.1 302 Moved Temporarily
2 | Content-Type: text/html;charset=utf-8
3 | Content-Length: 0
4 | Cache-Control: public
5 | Date: Tue, 10 Oct 2017 07:28:33 GMT
6 | Location: https://wetransfer.com/redirect
7 | Server: nginx/1.12.0
8 | X-Content-Type-Options: nosniff
9 | X-Request-Id: 8c43587ec891b2f1f72c61ecec2e96db
10 | X-XSS-Protection: 1; mode=block
11 | X-Cache: Miss from cloudfront
12 | Via: 1.1 72f202fb973968c0cfdb028ab6f36fac.cloudfront.net (CloudFront)
13 | X-Amz-Cf-Id: tU8eVZ9jWBJzd3aEB-4gyym_VxcPKskWFByEvXapy5WrdDkV-35-KA==
14 | Connection: Keep-alive
15 |
--------------------------------------------------------------------------------
/Tests/MockerTests/Resources/wetransfer_bot_avatar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WeTransfer/Mocker/77b5abb4c803ca8199bdc7c6f4a9e0c78dcb6a93/Tests/MockerTests/Resources/wetransfer_bot_avatar.png
--------------------------------------------------------------------------------
/fastlane/.gitignore:
--------------------------------------------------------------------------------
1 | installer/
2 | test_output/
3 | README.md
4 | report.xml
--------------------------------------------------------------------------------
/fastlane/Fastfile:
--------------------------------------------------------------------------------
1 | # Fastlane requirements
2 | fastlane_version "1.109.0"
3 |
4 | import "./../Submodules/WeTransfer-iOS-CI/Fastlane/testing_lanes.rb"
5 | import "./../Submodules/WeTransfer-iOS-CI/Fastlane/shared_lanes.rb"
6 |
7 | desc "Run the tests and prepare for Danger"
8 | lane :test do |options|
9 | test_package(
10 | package_name: 'Mocker',
11 | package_path: ENV['PWD'],
12 | disable_automatic_package_resolution: false
13 | )
14 | end
15 |
--------------------------------------------------------------------------------