├── .github ├── CODEOWNERS └── workflows │ ├── ci.yml │ └── stale.yml ├── .gitignore ├── .gitmodules ├── .swiftpm └── xcode │ ├── package.xcworkspace │ └── contents.xcworkspacedata │ └── xcshareddata │ └── xcschemes │ ├── Mocker.xcscheme │ └── MockerTests.xcscheme ├── Assets └── artwork.jpg ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Changelog.md ├── Gemfile ├── LICENSE ├── Package.swift ├── README.md ├── Sources └── Mocker │ ├── Mock+DataType.swift │ ├── Mock.swift │ ├── Mocker.swift │ ├── MockingURLProtocol.swift │ ├── OnRequestHandler.swift │ ├── URLMatchType.swift │ └── XCTest+Mocker.swift ├── Tests └── MockerTests │ ├── MockTests.swift │ ├── MockedData.swift │ ├── MockerTests.swift │ └── Resources │ ├── JSON Files │ └── example.json │ ├── sample-redirect-get.data │ └── wetransfer_bot_avatar.png └── fastlane ├── .gitignore └── Fastfile /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # https://help.github.com/en/articles/about-code-owners 2 | # These owners will be the default owners for everything in 3 | # the repo. Unless a later match takes precedence, they 4 | # will be requested for review when someone opens a PR. 5 | * @wetransfer/ios-collect 6 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: "Mocker SPM CI" 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - '*' 10 | 11 | jobs: 12 | macos-run-tests: 13 | name: Unit Tests (macOS) 14 | runs-on: macos-latest 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Run Tests 18 | run: swift test 19 | 20 | linux-run-tests: 21 | name: Unit Tests (Linux) 22 | runs-on: ubuntu-latest 23 | steps: 24 | - uses: actions/checkout@v2 25 | - name: Run Tests 26 | run: swift test 27 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: Mark stale issues and pull requests 2 | 3 | on: 4 | schedule: 5 | - cron: "0 0 * * *" 6 | 7 | jobs: 8 | stale: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/stale@v4 12 | with: 13 | repo-token: ${{ secrets.GITHUB_TOKEN }} 14 | stale-issue-label: 'Stale' 15 | stale-issue-message: 'This issue is stale because it has been open for 30 days with no activity. Remove the Stale label or comment or this will be closed in 10 days.' 16 | stale-pr-label: 'Stale' 17 | stale-pr-message: 'This PR is stale because it has been open for 30 days with no activity. Remove the Stale label or comment or this will be closed in 10 days.' 18 | exempt-issue-labels: 'bug,enhancement' 19 | days-before-stale: 30 20 | days-before-close: 10 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OS X 2 | .DS_Store 3 | 4 | # Xcode 5 | build/ 6 | *.pbxuser 7 | !default.pbxuser 8 | *.mode1v3 9 | !default.mode1v3 10 | *.mode2v3 11 | !default.mode2v3 12 | *.perspectivev3 13 | !default.perspectivev3 14 | xcuserdata 15 | *.xccheckout 16 | profile 17 | *.moved-aside 18 | DerivedData 19 | *.hmap 20 | *.xccheckout 21 | # AppCode 22 | .idea/ 23 | 24 | Carthage 25 | 26 | Demo/Pods 27 | .ruby-version 28 | .ruby-gemset 29 | # Swift Package Manager 30 | .build 31 | Packages 32 | Package.pins 33 | .spm-build -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "Submodules/WeTransfer-iOS-CI"] 2 | path = Submodules/WeTransfer-iOS-CI 3 | url = https://github.com/WeTransfer/WeTransfer-iOS-CI.git 4 | branch = master -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/Mocker.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 43 | 49 | 50 | 51 | 52 | 53 | 58 | 59 | 61 | 67 | 68 | 69 | 70 | 71 | 81 | 82 | 88 | 89 | 95 | 96 | 97 | 98 | 100 | 101 | 104 | 105 | 106 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/MockerTests.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 16 | 18 | 24 | 25 | 26 | 27 | 28 | 38 | 39 | 45 | 46 | 48 | 49 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /Assets/artwork.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeTransfer/Mocker/77b5abb4c803ca8199bdc7c6f4a9e0c78dcb6a93/Assets/artwork.jpg -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at mobile@wetransfer.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Mocker 2 | 3 | As the creators, and maintainers of this project, we're glad to share our projects and invite contributors to help us stay up to date. Please take a moment to review this document in order to make the contribution process easy and effective for everyone involved. 4 | 5 | Following these guidelines helps to communicate that you respect the time of the developers managing and developing this open source project. In return, they should reciprocate that respect in addressing your issue or assessing patches and features. 6 | 7 | In general, we expect you to follow our [Code of Conduct](https://github.com/WeTransfer/Mocker/blob/master/CODE_OF_CONDUCT.md). 8 | 9 | ## Using the issue tracker for bug reports, feature requests and discussions 10 | 11 | ### First time contributors 12 | We should encourage first time contributors. A good inspiration on this can be found [here](http://www.firsttimersonly.com/). As pointed out: 13 | 14 | > If you are an OSS project owner, then consider marking a few open issues with the label first-timers-only. The first-timers-only label explicitly announces: 15 | 16 | > "I'm willing to hold your hand so you can make your first PR. This issue is rather a bit easier than normal. And anyone who’s already contributed to open source isn’t allowed to touch this one!" 17 | 18 | By labeling issues with this `first-timers-only` label we help first time contributors step up their game and start contributing. 19 | 20 | ### Bug reports 21 | 22 | A bug is a _demonstrable problem_ that is caused by the code in the repository. 23 | Good bug reports are extremely helpful - thank you! 24 | 25 | Guidelines for bug reports: 26 | 27 | 1. **Use the GitHub issue search** — check if the issue has already been 28 | reported. 29 | 30 | 2. **Check if the issue has been fixed** — try to reproduce it using the 31 | latest `master` or development branch in the repository. 32 | 33 | 3. **Isolate the problem** — provide clear steps to reproduce. 34 | 35 | A good bug report shouldn't leave others needing to chase you up for more 36 | information. Please try to be as detailed as possible in your report. What is 37 | your environment? What steps will reproduce the issue? What would you expect to be the outcome? All these details will help people to fix any potential bugs. 38 | 39 | Example: 40 | 41 | > Short and descriptive example bug report title 42 | > 43 | > A summary of the issue and the OS environment in which it occurs. If 44 | > suitable, include the steps required to reproduce the bug. 45 | > 46 | > 1. This is the first step 47 | > 2. This is the second step 48 | > 3. Further steps, etc. 49 | > 50 | > `` - a link to the reduced test case, if possible 51 | > 52 | > Any other information you want to share that is relevant to the issue being 53 | > reported. This might include the lines of code that you have identified as 54 | > causing the bug, and potential solutions (and your opinions on their 55 | > merits). 56 | 57 | ### Feature requests 58 | 59 | Feature requests are welcome. But take a moment to find out whether your idea 60 | fits with the scope and aims of the project. It's up to *you* to make a strong 61 | case to convince the project's developers of the merits of this feature. Please 62 | provide as much detail and context as possible. 63 | 64 | Do check if the feature request already exists. If it does, give it a thumbs-up emoji 65 | or even comment. We'd like to avoid duplicate requests. 66 | 67 | ### Pull requests 68 | 69 | Good pull requests - patches, improvements, new features - are a fantastic 70 | help. They should remain focused in scope and avoid containing unrelated 71 | commits. 72 | 73 | **Please ask first** before embarking on any significant pull request (e.g. 74 | implementing features, refactoring code, porting to a different language), 75 | otherwise you risk spending a lot of time working on something that the 76 | project's developers might not want to merge into the project. As far as _where_ to ask, 77 | the feature request or bug report is the best place to go. 78 | 79 | Please adhere to the coding conventions used throughout a project (indentation, 80 | accurate comments, etc.) and any other requirements (such as test coverage). 81 | 82 | Follow this process if you'd like your work considered for inclusion in the 83 | project: 84 | 85 | 1. [Fork](http://help.github.com/fork-a-repo/) the project, clone your fork, 86 | and configure the remotes: 87 | 88 | ```bash 89 | # Clone your fork of the repo into the current directory 90 | git clone git@github.com:WeTransfer/Mocker.git 91 | # Navigate to the newly cloned directory 92 | cd Mocker 93 | # Assign the original repo to a remote called "upstream" 94 | git remote add upstream git@github.com:WeTransfer/Mocker.git 95 | ``` 96 | 97 | 2. If you cloned a while ago, get the latest changes from upstream: 98 | 99 | ```bash 100 | git checkout 101 | git pull upstream 102 | ``` 103 | 104 | 3. Create a new topic branch (off the main project development branch) to 105 | contain your feature, change, or fix: 106 | 107 | ```bash 108 | git checkout -b 109 | ``` 110 | 111 | 4. Commit your changes in logical chunks. 112 | 113 | 5. Locally merge (or rebase) the upstream development branch into your topic branch: 114 | 115 | ```bash 116 | git pull [--rebase] upstream 117 | ``` 118 | 119 | 6. Push your topic branch up to your fork: 120 | 121 | ```bash 122 | git push origin 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 | --------------------------------------------------------------------------------