├── .formatter.exs ├── .github └── workflows │ └── tests.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── config ├── config.exs └── test.exs ├── fixture ├── custom_cassettes │ ├── finch_generic_string_error.json │ ├── finch_generic_timeout_error.json │ ├── finch_httperror.json │ ├── finch_tuple_transport_error.json │ ├── httpoison_get_alternate.json │ ├── method_mocking.json │ ├── response_mocking.json │ ├── response_mocking_regex.json │ └── response_mocking_with_param.json └── vcr_cassettes │ ├── different_headers_off.json │ ├── different_headers_on.json │ ├── different_query_params_off.json │ ├── different_query_params_on.json │ ├── different_request_body_params_off.json │ ├── different_request_body_params_on.json │ ├── error_finch.json │ ├── error_hackney.json │ ├── error_ibrowse.json │ ├── example_finch.json │ ├── example_finch_different.json │ ├── example_finch_multiple.json │ ├── example_httpc_request_1.json │ ├── example_httpc_request_4.json │ ├── example_httpc_request_4_additional_options.json │ ├── example_httpc_request_error.json │ ├── example_httpotion.json │ ├── example_ibrowse.json │ ├── example_ibrowse_different.json │ ├── example_ibrowse_multiple.json │ ├── finch_delete.json │ ├── finch_get_localhost.json │ ├── finch_get_timeout.json │ ├── finch_patch.json │ ├── finch_post.json │ ├── finch_post_map.json │ ├── finch_put.json │ ├── hackney_get.json │ ├── hackney_get_gzipped.json │ ├── hackney_get_localhost.json │ ├── hackney_head.json │ ├── hackney_invalid_client.json │ ├── hackney_path_encode_fun.json │ ├── hackney_with_body.json │ ├── httpc_get_localhost.json │ ├── httpoison_delete.json │ ├── httpoison_get.json │ ├── httpoison_get_basic_auth.json │ ├── httpoison_get_error.json │ ├── httpoison_head.json │ ├── httpoison_mutipart_post.json │ ├── httpoison_patch.json │ ├── httpoison_post.json │ ├── httpoison_post_form.json │ ├── httpoison_post_ssl.json │ ├── httpoison_put.json │ ├── ibrowse_get_localhost.json │ ├── ignore_localhost_on.json │ ├── ignore_localhost_unset.json │ ├── ignore_localhost_with_headers.json │ ├── ignore_urls_on.json │ ├── ignore_urls_unset.json │ ├── ignore_urls_with_headers.json │ ├── option_clean_all.json │ ├── option_clean_each.json │ ├── return_value_from_block.json │ ├── return_value_from_block_throws_error.json │ ├── user_defined_matchers_matching.json │ └── user_defined_matchers_not_matching.json ├── lib ├── exvcr.ex ├── exvcr │ ├── actor.ex │ ├── adapter.ex │ ├── adapter │ │ ├── finch.ex │ │ ├── finch │ │ │ └── converter.ex │ │ ├── hackney.ex │ │ ├── hackney │ │ │ ├── converter.ex │ │ │ └── store.ex │ │ ├── httpc.ex │ │ ├── httpc │ │ │ └── converter.ex │ │ ├── ibrowse.ex │ │ └── ibrowse │ │ │ └── converter.ex │ ├── application.ex │ ├── checker.ex │ ├── config.ex │ ├── config_loader.ex │ ├── converter.ex │ ├── exceptions.ex │ ├── filter.ex │ ├── handler.ex │ ├── iex.ex │ ├── json.ex │ ├── mock.ex │ ├── mock_lock.ex │ ├── recorder.ex │ ├── records.ex │ ├── setting.ex │ ├── task │ │ ├── runner.ex │ │ ├── show.ex │ │ └── util.ex │ └── util.ex └── mix │ └── tasks.ex ├── mix.exs ├── mix.lock ├── package.exs └── test ├── adapter_finch_test.exs ├── adapter_hackney_test.exs ├── adapter_httpc_test.exs ├── adapter_ibrowse_test.exs ├── cassettes ├── test1.json └── test2.json ├── config_loader_test.exs ├── config_test.exs ├── enable_global_settings_test.exs ├── filter_test.exs ├── handler_custom_mode_test.exs ├── handler_options_test.exs ├── handler_stub_mode_test.exs ├── iex_test.exs ├── ignore_localhost_test.exs ├── ignore_urls_test.exs ├── mix └── tasks_test.exs ├── mock_lock_test.exs ├── recorder_base_test.exs ├── recorder_finch_test.exs ├── recorder_hackney_test.exs ├── recorder_httpc_test.exs ├── recorder_ibrowse_test.exs ├── setting_test.exs ├── strict_mode_test.exs ├── task_runner_test.exs ├── task_util_test.exs └── test_helper.exs /.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | inputs: [ 3 | "mix.exs", 4 | ".formatter.exs", 5 | "config/*.exs", 6 | "lib/**/*.ex", 7 | "test/**/*_test.exs" 8 | ], 9 | line_length: 120 10 | ] 11 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | tests: 7 | name: Run Tests 8 | runs-on: ubuntu-22.04 9 | continue-on-error: ${{ matrix.experimental }} 10 | strategy: 11 | fail-fast: true 12 | matrix: 13 | include: 14 | - otp: "25" 15 | elixir: "1.14" 16 | global-mock: true 17 | experimental: false 18 | - otp: "25" 19 | elixir: "1.14" 20 | global-mock: false 21 | experimental: false 22 | - otp: "27" 23 | elixir: "1.17" 24 | global-mock: true 25 | experimental: false 26 | - otp: "27" 27 | elixir: "1.17" 28 | global-mock: false 29 | experimental: false 30 | - otp: "27.2" 31 | elixir: "1.18.0" 32 | global-mock: true 33 | experimental: true 34 | - otp: "27.2" 35 | elixir: "1.18.0" 36 | global-mock: false 37 | experimental: true 38 | lint: true 39 | env: 40 | MIX_ENV: test 41 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 42 | GLOBAL_MOCK: ${{ matrix.global-mock }} 43 | steps: 44 | - uses: actions/checkout@v4 45 | - uses: erlef/setup-beam@v1 46 | with: 47 | otp-version: ${{ matrix.otp }} 48 | elixir-version: ${{ matrix.elixir }} 49 | - uses: actions/cache@v4 50 | with: 51 | path: | 52 | deps 53 | _build 54 | key: ${{ runner.os }}-${{ matrix.otp }}-${{ matrix.elixir }}-mix-${{ hashFiles('**/mix.lock') }} 55 | restore-keys: | 56 | ${{ runner.os }}-${{ matrix.otp }}-${{ matrix.elixir }}-mix- 57 | - name: Install dependencies 58 | run: mix deps.get 59 | - name: Compile project 60 | run: mix compile 61 | - name: Check formatting 62 | run: mix format --check-formatted 63 | if: ${{ matrix.lint }} 64 | - name: Run tests 65 | run: mix test 66 | - uses: nick-invision/retry@v3 67 | with: 68 | timeout_minutes: 3 69 | max_attempts: 3 70 | shell: bash 71 | command: mix coveralls.github 72 | continue-on-error: true 73 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /ebin 2 | /deps 3 | erl_crash.dump 4 | *.ez 5 | /tmp 6 | /_build 7 | /doc 8 | .dockerignore 9 | .devcontainer 10 | Dockerfile 11 | docker-compose.yml 12 | .vscode 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013-2015 parroty 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | config :exvcr, 4 | global_mock: false, 5 | vcr_cassette_library_dir: "fixture/vcr_cassettes", 6 | custom_cassette_library_dir: "fixture/custom_cassettes", 7 | filter_sensitive_data: [ 8 | [pattern: ".+", placeholder: "PASSWORD_PLACEHOLDER"] 9 | ], 10 | filter_url_params: false, 11 | filter_request_headers: [], 12 | response_headers_blacklist: [], 13 | ignore_localhost: false, 14 | enable_global_settings: false, 15 | strict_mode: false 16 | 17 | if Mix.env() == :test, do: import_config("test.exs") 18 | -------------------------------------------------------------------------------- /config/test.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | config :exvcr, 4 | global_mock: System.get_env("GLOBAL_MOCK") == "true" 5 | -------------------------------------------------------------------------------- /fixture/custom_cassettes/finch_generic_string_error.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "body": "", 5 | "headers": [], 6 | "method": "get", 7 | "options": [], 8 | "request_body": "", 9 | "url": "http://example.com/" 10 | }, 11 | "response": { 12 | "binary": false, 13 | "body": "%{reason: \"some made up error which could happen, in theory\"}", 14 | "headers": [], 15 | "status_code": null, 16 | "type": "error" 17 | } 18 | } 19 | ] 20 | -------------------------------------------------------------------------------- /fixture/custom_cassettes/finch_generic_timeout_error.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "body": "", 5 | "headers": [], 6 | "method": "get", 7 | "options": [], 8 | "request_body": "", 9 | "url": "http://example.com/" 10 | }, 11 | "response": { 12 | "binary": false, 13 | "body": "%{reason: :timeout}", 14 | "headers": [], 15 | "status_code": null, 16 | "type": "error" 17 | } 18 | } 19 | ] 20 | -------------------------------------------------------------------------------- /fixture/custom_cassettes/finch_httperror.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "body": "", 5 | "headers": [], 6 | "method": "get", 7 | "options": [], 8 | "request_body": "", 9 | "url": "http://example.com/" 10 | }, 11 | "response": { 12 | "binary": false, 13 | "body": "%Mint.HTTPError{module: Mint.HTTP2, reason: :too_many_concurrent_requests}", 14 | "headers": [], 15 | "status_code": null, 16 | "type": "error" 17 | } 18 | } 19 | ] 20 | -------------------------------------------------------------------------------- /fixture/custom_cassettes/finch_tuple_transport_error.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "body": "", 5 | "headers": [], 6 | "method": "get", 7 | "options": [], 8 | "request_body": "", 9 | "url": "http://example.com/" 10 | }, 11 | "response": { 12 | "binary": false, 13 | "body": "%Mint.TransportError{reason: {:bad_alpn_protocol, \"h3\"}}", 14 | "headers": [], 15 | "status_code": null, 16 | "type": "error" 17 | } 18 | } 19 | ] 20 | -------------------------------------------------------------------------------- /fixture/custom_cassettes/httpoison_get_alternate.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "url": "http://example.com" 5 | }, 6 | "response": { 7 | "body": "Example Domain 1", 8 | "status_code": 200 9 | } 10 | }, 11 | { 12 | "request": { 13 | "url": "http://example.com" 14 | }, 15 | "response": { 16 | "body": "Example Domain 2", 17 | "status_code": 200 18 | } 19 | } 20 | ] 21 | -------------------------------------------------------------------------------- /fixture/custom_cassettes/method_mocking.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "url": "http://example.com", 5 | "method": "post" 6 | }, 7 | "response": { 8 | "status_code": 200, 9 | "headers": { 10 | "Content-Type": "text/html" 11 | }, 12 | "body": "

Custom Response

" 13 | } 14 | } 15 | ] -------------------------------------------------------------------------------- /fixture/custom_cassettes/response_mocking.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "url": "http://example.com" 5 | }, 6 | "response": { 7 | "status_code": 200, 8 | "headers": { 9 | "Content-Type": "text/html" 10 | }, 11 | "body": "

Custom Response

" 12 | } 13 | } 14 | ] -------------------------------------------------------------------------------- /fixture/custom_cassettes/response_mocking_regex.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "url": "~r/http://example.com/.+/abc/" 5 | }, 6 | "response": { 7 | "status_code": 200, 8 | "headers": { 9 | "Content-Type": "text/html" 10 | }, 11 | "body": "

Custom Response

" 12 | } 13 | } 14 | ] 15 | -------------------------------------------------------------------------------- /fixture/custom_cassettes/response_mocking_with_param.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "url": "http://example.com?auth_token=123abc&another_param=456", 5 | "method": "get" 6 | }, 7 | "response": { 8 | "status_code": 200, 9 | "headers": { 10 | "Content-Type": "text/html" 11 | }, 12 | "body": "

Custom Response

" 13 | } 14 | } 15 | ] -------------------------------------------------------------------------------- /fixture/vcr_cassettes/different_headers_off.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "body": "p=3", 5 | "headers": { 6 | "User-Agent": "My App" 7 | }, 8 | "method": "post", 9 | "options": [], 10 | "request_body": "", 11 | "url": "http://localhost:34006/server" 12 | }, 13 | "response": { 14 | "body": "test_response_before", 15 | "headers": { 16 | "connection": "keep-alive", 17 | "server": "Cowboy", 18 | "date": "Thu, 21 Apr 2016 17:52:30 GMT", 19 | "content-length": "20" 20 | }, 21 | "status_code": 200, 22 | "type": "ok" 23 | } 24 | } 25 | ] -------------------------------------------------------------------------------- /fixture/vcr_cassettes/different_headers_on.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "body": "body", 5 | "headers": { 6 | "User-Agent": "My App" 7 | }, 8 | "method": "post", 9 | "options": [], 10 | "request_body": "", 11 | "url": "http://localhost:34006/server" 12 | }, 13 | "response": { 14 | "body": "test_response_before", 15 | "headers": { 16 | "connection": "keep-alive", 17 | "server": "Cowboy", 18 | "date": "Thu, 21 Apr 2016 17:40:10 GMT", 19 | "content-length": "20" 20 | }, 21 | "status_code": 200, 22 | "type": "ok" 23 | } 24 | }, 25 | { 26 | "request": { 27 | "body": "body", 28 | "headers": { 29 | "User-Agent": "Other App" 30 | }, 31 | "method": "post", 32 | "options": [], 33 | "request_body": "", 34 | "url": "http://localhost:34006/server" 35 | }, 36 | "response": { 37 | "body": "test_response_after", 38 | "headers": { 39 | "connection": "keep-alive", 40 | "server": "Cowboy", 41 | "date": "Thu, 21 Apr 2016 17:40:10 GMT", 42 | "content-length": "19" 43 | }, 44 | "status_code": 200, 45 | "type": "ok" 46 | } 47 | } 48 | ] -------------------------------------------------------------------------------- /fixture/vcr_cassettes/different_query_params_off.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "body": "", 5 | "headers": [], 6 | "method": "get", 7 | "options": [], 8 | "url": "http://localhost:34006/server?p=3" 9 | }, 10 | "response": { 11 | "body": "test_response_before", 12 | "headers": { 13 | "connection": "keep-alive", 14 | "server": "Cowboy", 15 | "date": "Sun, 04 Jan 2015 12:52:56 GMT", 16 | "content-length": "20" 17 | }, 18 | "status_code": 200, 19 | "type": "ok" 20 | } 21 | } 22 | ] -------------------------------------------------------------------------------- /fixture/vcr_cassettes/different_query_params_on.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "body": "", 5 | "headers": [], 6 | "method": "get", 7 | "options": [], 8 | "url": "http://localhost:34006/server?p=3&q=string" 9 | }, 10 | "response": { 11 | "body": "test_response_before", 12 | "headers": { 13 | "connection": "keep-alive", 14 | "server": "Cowboy", 15 | "date": "Sun, 04 Jan 2015 12:52:56 GMT", 16 | "content-length": "20" 17 | }, 18 | "status_code": 200, 19 | "type": "ok" 20 | } 21 | }, 22 | { 23 | "request": { 24 | "body": "", 25 | "headers": [], 26 | "method": "get", 27 | "options": [], 28 | "url": "http://localhost:34006/server?p=4" 29 | }, 30 | "response": { 31 | "body": "test_response_after", 32 | "headers": { 33 | "connection": "keep-alive", 34 | "server": "Cowboy", 35 | "date": "Sun, 04 Jan 2015 12:52:56 GMT", 36 | "content-length": "19" 37 | }, 38 | "status_code": 200, 39 | "type": "ok" 40 | } 41 | } 42 | ] -------------------------------------------------------------------------------- /fixture/vcr_cassettes/different_request_body_params_off.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "body": "p=3", 5 | "headers": [], 6 | "method": "post", 7 | "options": [], 8 | "request_body": "", 9 | "url": "http://localhost:34006/server" 10 | }, 11 | "response": { 12 | "body": "test_response_before", 13 | "headers": { 14 | "connection": "keep-alive", 15 | "server": "Cowboy", 16 | "date": "Wed, 30 Sep 2015 13:32:48 GMT", 17 | "content-length": "20" 18 | }, 19 | "status_code": 200, 20 | "type": "ok" 21 | } 22 | } 23 | ] -------------------------------------------------------------------------------- /fixture/vcr_cassettes/different_request_body_params_on.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "body": "p=3", 5 | "headers": [], 6 | "method": "post", 7 | "options": [], 8 | "request_body": "", 9 | "url": "http://localhost:34006/server" 10 | }, 11 | "response": { 12 | "body": "test_response_before", 13 | "headers": { 14 | "connection": "keep-alive", 15 | "server": "Cowboy", 16 | "date": "Wed, 30 Sep 2015 13:12:45 GMT", 17 | "content-length": "20" 18 | }, 19 | "status_code": 200, 20 | "type": "ok" 21 | } 22 | }, 23 | { 24 | "request": { 25 | "body": "p=4", 26 | "headers": [], 27 | "method": "post", 28 | "options": [], 29 | "request_body": "", 30 | "url": "http://localhost:34006/server" 31 | }, 32 | "response": { 33 | "body": "test_response_after", 34 | "headers": { 35 | "connection": "keep-alive", 36 | "server": "Cowboy", 37 | "date": "Wed, 30 Sep 2015 13:12:45 GMT", 38 | "content-length": "19" 39 | }, 40 | "status_code": 200, 41 | "type": "ok" 42 | } 43 | } 44 | ] -------------------------------------------------------------------------------- /fixture/vcr_cassettes/error_finch.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "body": "", 5 | "headers": [], 6 | "method": "get", 7 | "options": [], 8 | "request_body": "", 9 | "url": "http://invalid_url/" 10 | }, 11 | "response": { 12 | "binary": false, 13 | "body": "%Mint.TransportError{reason: :nxdomain}", 14 | "headers": [], 15 | "status_code": null, 16 | "type": "error" 17 | } 18 | } 19 | ] -------------------------------------------------------------------------------- /fixture/vcr_cassettes/error_hackney.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "body": "", 5 | "headers": [], 6 | "method": "get", 7 | "options": [], 8 | "url": "http://invalid_url" 9 | }, 10 | "response": { 11 | "body": "nxdomain", 12 | "headers": [], 13 | "status_code": null, 14 | "type": "error" 15 | } 16 | } 17 | ] -------------------------------------------------------------------------------- /fixture/vcr_cassettes/error_ibrowse.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "body": "", 5 | "headers": [], 6 | "method": "get", 7 | "options": [], 8 | "url": "http://invalid_url" 9 | }, 10 | "response": { 11 | "body": [ 12 | "conn_failed", 13 | [ 14 | "error", 15 | "nxdomain" 16 | ] 17 | ], 18 | "headers": [], 19 | "status_code": null, 20 | "type": "error" 21 | } 22 | } 23 | ] -------------------------------------------------------------------------------- /fixture/vcr_cassettes/example_finch.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "body": "", 5 | "headers": [], 6 | "method": "get", 7 | "options": [], 8 | "request_body": "", 9 | "url": "http://example.com/" 10 | }, 11 | "response": { 12 | "binary": false, 13 | "body": "\n\n\n Example Domain\n\n \n \n \n \n\n\n\n
\n

Example Domain

\n

This domain is for use in illustrative examples in documents. You may use this\n domain in literature without prior coordination or asking for permission.

\n

More information...

\n
\n\n\n", 14 | "headers": { 15 | "age": "355150", 16 | "cache-control": "max-age=604800", 17 | "content-type": "text/html; charset=UTF-8", 18 | "date": "Wed, 28 Jul 2021 19:21:33 GMT", 19 | "etag": "\"3147526947+ident\"", 20 | "expires": "Wed, 04 Aug 2021 19:21:33 GMT", 21 | "last-modified": "Thu, 17 Oct 2019 07:18:26 GMT", 22 | "server": "ECS (bsa/EB17)", 23 | "vary": "Accept-Encoding", 24 | "x-cache": "HIT", 25 | "content-length": "1256" 26 | }, 27 | "status_code": 200, 28 | "type": "ok" 29 | } 30 | } 31 | ] -------------------------------------------------------------------------------- /fixture/vcr_cassettes/example_finch_different.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "body": "", 5 | "headers": [], 6 | "method": "get", 7 | "options": [], 8 | "request_body": "", 9 | "url": "http://example.com/" 10 | }, 11 | "response": { 12 | "binary": false, 13 | "body": "\n\n\n Example Domain\n\n \n \n \n \n\n\n\n
\n

Example Domain

\n

This domain is for use in illustrative examples in documents. You may use this\n domain in literature without prior coordination or asking for permission.

\n

More information...

\n
\n\n\n", 14 | "headers": { 15 | "age": "538495", 16 | "cache-control": "max-age=604800", 17 | "content-type": "text/html; charset=UTF-8", 18 | "date": "Wed, 28 Jul 2021 19:21:35 GMT", 19 | "etag": "\"3147526947+ident\"", 20 | "expires": "Wed, 04 Aug 2021 19:21:35 GMT", 21 | "last-modified": "Thu, 17 Oct 2019 07:18:26 GMT", 22 | "server": "ECS (bsa/EB23)", 23 | "vary": "Accept-Encoding", 24 | "x-cache": "HIT", 25 | "content-length": "1256" 26 | }, 27 | "status_code": 200, 28 | "type": "ok" 29 | } 30 | } 31 | ] -------------------------------------------------------------------------------- /fixture/vcr_cassettes/example_finch_multiple.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "body": "", 5 | "headers": [], 6 | "method": "get", 7 | "options": [], 8 | "request_body": "", 9 | "url": "http://example.com/" 10 | }, 11 | "response": { 12 | "binary": false, 13 | "body": "\n\n\n Example Domain\n\n \n \n \n \n\n\n\n
\n

Example Domain

\n

This domain is for use in illustrative examples in documents. You may use this\n domain in literature without prior coordination or asking for permission.

\n

More information...

\n
\n\n\n", 14 | "headers": { 15 | "age": "355150", 16 | "cache-control": "max-age=604800", 17 | "content-type": "text/html; charset=UTF-8", 18 | "date": "Wed, 28 Jul 2021 19:21:33 GMT", 19 | "etag": "\"3147526947+ident\"", 20 | "expires": "Wed, 04 Aug 2021 19:21:33 GMT", 21 | "last-modified": "Thu, 17 Oct 2019 07:18:26 GMT", 22 | "server": "ECS (bsa/EB17)", 23 | "vary": "Accept-Encoding", 24 | "x-cache": "HIT", 25 | "content-length": "1256" 26 | }, 27 | "status_code": 200, 28 | "type": "ok" 29 | } 30 | }, 31 | { 32 | "request": { 33 | "body": "", 34 | "headers": [], 35 | "method": "get", 36 | "options": [], 37 | "request_body": "", 38 | "url": "http://example.com/2" 39 | }, 40 | "response": { 41 | "binary": false, 42 | "body": "\n\n\n Example Domain\n\n \n \n \n \n\n\n\n
\n

Example Domain

\n

This domain is for use in illustrative examples in documents. You may use this\n domain in literature without prior coordination or asking for permission.

\n

More information...

\n
\n\n\n", 43 | "headers": { 44 | "accept-ranges": "bytes", 45 | "age": "213974", 46 | "cache-control": "max-age=604800", 47 | "content-type": "text/html; charset=UTF-8", 48 | "date": "Wed, 28 Jul 2021 19:21:34 GMT", 49 | "expires": "Wed, 04 Aug 2021 19:21:34 GMT", 50 | "last-modified": "Mon, 26 Jul 2021 07:55:21 GMT", 51 | "server": "ECS (bsa/EB16)", 52 | "vary": "Accept-Encoding", 53 | "x-cache": "404-HIT", 54 | "content-length": "1256" 55 | }, 56 | "status_code": 404, 57 | "type": "ok" 58 | } 59 | } 60 | ] -------------------------------------------------------------------------------- /fixture/vcr_cassettes/example_httpc_request_1.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "body": "", 5 | "headers": [], 6 | "method": "get", 7 | "options": { 8 | "httpc_options": [], 9 | "http_options": [] 10 | }, 11 | "url": "http://example.com" 12 | }, 13 | "response": { 14 | "body": "\n\n\n Example Domain\n\n \n \n \n \n\n\n\n
\n

Example Domain

\n

This domain is established to be used for illustrative examples in documents. You may use this\n domain in examples without prior coordination or asking for permission.

\n

More information...

\n
\n\n\n", 15 | "headers": { 16 | "cache-control": "max-age=604800", 17 | "date": "Sun, 30 Nov 2014 11:15:00 GMT", 18 | "accept-ranges": "bytes", 19 | "etag": "\"359670651\"", 20 | "server": "ECS (cpm/F9FC)", 21 | "content-length": "1270", 22 | "content-type": "text/html", 23 | "expires": "Sun, 07 Dec 2014 11:15:00 GMT", 24 | "last-modified": "Fri, 09 Aug 2013 23:54:35 GMT", 25 | "x-cache": "HIT", 26 | "x-ec-custom-error": "1" 27 | }, 28 | "status_code": [ 29 | "HTTP/1.1", 30 | 200, 31 | "OK" 32 | ], 33 | "type": "ok" 34 | } 35 | } 36 | ] -------------------------------------------------------------------------------- /fixture/vcr_cassettes/example_httpc_request_4.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "body": "", 5 | "headers": [], 6 | "method": "get", 7 | "options": { 8 | "httpc_options": [], 9 | "http_options": [] 10 | }, 11 | "request_body": "", 12 | "url": "http://example.com" 13 | }, 14 | "response": { 15 | "body": "\n\n\n Example Domain\n\n \n \n \n \n\n\n\n
\n

Example Domain

\n

This domain is established to be used for illustrative examples in documents. You may use this\n domain in examples without prior coordination or asking for permission.

\n

More information...

\n
\n\n\n", 16 | "headers": { 17 | "cache-control": "max-age=604800", 18 | "date": "Tue, 12 Jan 2016 13:12:40 GMT", 19 | "accept-ranges": "bytes", 20 | "etag": "\"359670651\"", 21 | "server": "ECS (ewr/15BD)", 22 | "vary": "Accept-Encoding", 23 | "content-length": "1270", 24 | "content-type": "text/html", 25 | "expires": "Tue, 19 Jan 2016 13:12:40 GMT", 26 | "last-modified": "Fri, 09 Aug 2013 23:54:35 GMT", 27 | "x-cache": "HIT", 28 | "x-ec-custom-error": "1" 29 | }, 30 | "status_code": [ 31 | "HTTP/1.1", 32 | 200, 33 | "OK" 34 | ], 35 | "type": "ok" 36 | } 37 | } 38 | ] -------------------------------------------------------------------------------- /fixture/vcr_cassettes/example_httpc_request_4_additional_options.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "body": "", 5 | "headers": { 6 | "Content-Type": "text/html" 7 | }, 8 | "method": "get", 9 | "options": { 10 | "httpc_options": { 11 | "body_format": "binary" 12 | }, 13 | "http_options": { 14 | "connect_timeout": "3000", 15 | "timeout": "5000" 16 | } 17 | }, 18 | "request_body": "", 19 | "url": "http://example.com" 20 | }, 21 | "response": { 22 | "body": "\n\n\n Example Domain\n\n \n \n \n \n\n\n\n
\n

Example Domain

\n

This domain is established to be used for illustrative examples in documents. You may use this\n domain in examples without prior coordination or asking for permission.

\n

More information...

\n
\n\n\n", 23 | "headers": { 24 | "cache-control": "max-age=604800", 25 | "date": "Tue, 12 Jan 2016 13:12:40 GMT", 26 | "accept-ranges": "bytes", 27 | "etag": "\"359670651\"", 28 | "server": "ECS (ewr/15BD)", 29 | "vary": "Accept-Encoding", 30 | "content-length": "1270", 31 | "content-type": "text/html", 32 | "expires": "Tue, 19 Jan 2016 13:12:40 GMT", 33 | "last-modified": "Fri, 09 Aug 2013 23:54:35 GMT", 34 | "x-cache": "HIT", 35 | "x-ec-custom-error": "1" 36 | }, 37 | "status_code": [ 38 | "HTTP/1.1", 39 | 200, 40 | "OK" 41 | ], 42 | "type": "ok" 43 | } 44 | } 45 | ] -------------------------------------------------------------------------------- /fixture/vcr_cassettes/example_httpc_request_error.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "body": "", 5 | "headers": [], 6 | "method": "get", 7 | "options": { 8 | "httpc_options": [], 9 | "http_options": [] 10 | }, 11 | "url": "http://invalidurl" 12 | }, 13 | "response": { 14 | "body": "failed_connect", 15 | "headers": [], 16 | "status_code": null, 17 | "type": "error" 18 | } 19 | } 20 | ] -------------------------------------------------------------------------------- /fixture/vcr_cassettes/example_httpotion.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "body": "", 5 | "headers": [], 6 | "method": "get", 7 | "options": [], 8 | "url": "http://example.com" 9 | }, 10 | "response": { 11 | "body": "\n\n\n Example Domain\n\n \n \n \n \n\n\n\n
\n

Example Domain

\n

This domain is established to be used for illustrative examples in documents. You may use this\n domain in examples without prior coordination or asking for permission.

\n

More information...

\n
\n\n\n", 12 | "headers": { 13 | "Accept-Ranges": "bytes", 14 | "Cache-Control": "max-age=604800", 15 | "Content-Type": "text/html", 16 | "Date": "Sun, 30 Nov 2014 11:15:01 GMT", 17 | "Etag": "\"359670651\"", 18 | "Expires": "Sun, 07 Dec 2014 11:15:01 GMT", 19 | "Last-Modified": "Fri, 09 Aug 2013 23:54:35 GMT", 20 | "Server": "ECS (cpm/F9FC)", 21 | "X-Cache": "HIT", 22 | "x-ec-custom-error": "1", 23 | "Content-Length": "1270" 24 | }, 25 | "status_code": 200, 26 | "type": "ok" 27 | } 28 | } 29 | ] -------------------------------------------------------------------------------- /fixture/vcr_cassettes/example_ibrowse.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "body": "", 5 | "headers": [], 6 | "method": "get", 7 | "options": [], 8 | "url": "http://example.com" 9 | }, 10 | "response": { 11 | "body": "\n\n\n Example Domain\n\n \n \n \n \n\n\n\n
\n

Example Domain

\n

This domain is established to be used for illustrative examples in documents. You may use this\n domain in examples without prior coordination or asking for permission.

\n

More information...

\n
\n\n\n", 12 | "headers": { 13 | "Accept-Ranges": "bytes", 14 | "Cache-Control": "max-age=604800", 15 | "Content-Type": "text/html", 16 | "Date": "Sun, 30 Nov 2014 11:15:02 GMT", 17 | "Etag": "\"359670651\"", 18 | "Expires": "Sun, 07 Dec 2014 11:15:02 GMT", 19 | "Last-Modified": "Fri, 09 Aug 2013 23:54:35 GMT", 20 | "Server": "ECS (cpm/F9FC)", 21 | "X-Cache": "HIT", 22 | "x-ec-custom-error": "1", 23 | "Content-Length": "1270" 24 | }, 25 | "status_code": 200, 26 | "type": "ok" 27 | } 28 | } 29 | ] -------------------------------------------------------------------------------- /fixture/vcr_cassettes/example_ibrowse_different.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "body": "", 5 | "headers": [], 6 | "method": "get", 7 | "options": [], 8 | "url": "http://example.com" 9 | }, 10 | "response": { 11 | "body": "\n\n\n Example Domain\n\n \n \n \n \n\n\n\n
\n

Example Domain

\n

This domain is established to be used for illustrative examples in documents. You may use this\n domain in examples without prior coordination or asking for permission.

\n

More information...

\n
\n\n\n", 12 | "headers": { 13 | "Accept-Ranges": "bytes", 14 | "Cache-Control": "max-age=604800", 15 | "Content-Type": "text/html", 16 | "Date": "Sun, 30 Nov 2014 11:15:05 GMT", 17 | "Etag": "\"359670651\"", 18 | "Expires": "Sun, 07 Dec 2014 11:15:05 GMT", 19 | "Last-Modified": "Fri, 09 Aug 2013 23:54:35 GMT", 20 | "Server": "ECS (cpm/F9FC)", 21 | "X-Cache": "HIT", 22 | "x-ec-custom-error": "1", 23 | "Content-Length": "1270" 24 | }, 25 | "status_code": 200, 26 | "type": "ok" 27 | } 28 | } 29 | ] -------------------------------------------------------------------------------- /fixture/vcr_cassettes/example_ibrowse_multiple.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "body": "", 5 | "headers": [], 6 | "method": "get", 7 | "options": [], 8 | "url": "http://example.com" 9 | }, 10 | "response": { 11 | "body": "\n\n\n Example Domain\n\n \n \n \n \n\n\n\n
\n

Example Domain

\n

This domain is established to be used for illustrative examples in documents. You may use this\n domain in examples without prior coordination or asking for permission.

\n

More information...

\n
\n\n\n", 12 | "headers": { 13 | "Accept-Ranges": "bytes", 14 | "Cache-Control": "max-age=604800", 15 | "Content-Type": "text/html", 16 | "Date": "Sun, 30 Nov 2014 11:15:04 GMT", 17 | "Etag": "\"359670651\"", 18 | "Expires": "Sun, 07 Dec 2014 11:15:04 GMT", 19 | "Last-Modified": "Fri, 09 Aug 2013 23:54:35 GMT", 20 | "Server": "ECS (cpm/F9FC)", 21 | "X-Cache": "HIT", 22 | "x-ec-custom-error": "1", 23 | "Content-Length": "1270" 24 | }, 25 | "status_code": 200, 26 | "type": "ok" 27 | } 28 | }, 29 | { 30 | "request": { 31 | "body": "", 32 | "headers": [], 33 | "method": "get", 34 | "options": [], 35 | "url": "http://example.com/2" 36 | }, 37 | "response": { 38 | "body": "\n\n\n Example Domain\n\n \n \n \n \n\n\n\n
\n

Example Domain

\n

This domain is established to be used for illustrative examples in documents. You may use this\n domain in examples without prior coordination or asking for permission.

\n

More information...

\n
\n\n\n", 39 | "headers": { 40 | "Accept-Ranges": "bytes", 41 | "Cache-Control": "max-age=604800", 42 | "Content-Type": "text/html", 43 | "Date": "Sun, 30 Nov 2014 11:15:04 GMT", 44 | "Etag": "\"359670651\"", 45 | "Expires": "Sun, 07 Dec 2014 11:15:04 GMT", 46 | "Last-Modified": "Fri, 09 Aug 2013 23:54:35 GMT", 47 | "Server": "ECS (cpm/F9ED)", 48 | "X-Cache": "404-HIT", 49 | "x-ec-custom-error": "1", 50 | "Content-Length": "1270" 51 | }, 52 | "status_code": 404, 53 | "type": "ok" 54 | } 55 | } 56 | ] -------------------------------------------------------------------------------- /fixture/vcr_cassettes/finch_delete.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "body": "", 5 | "headers": [], 6 | "method": "delete", 7 | "options": { 8 | "receive_timeout": 10000 9 | }, 10 | "request_body": "", 11 | "url": "http://httpbin.org/delete" 12 | }, 13 | "response": { 14 | "binary": false, 15 | "body": "{\n \"args\": {}, \n \"data\": \"\", \n \"files\": {}, \n \"form\": {}, \n \"headers\": {\n \"Host\": \"httpbin.org\", \n \"User-Agent\": \"mint/1.3.0\", \n \"X-Amzn-Trace-Id\": \"Root=1-6101ae3f-0ad32f626360baed57e12388\"\n }, \n \"json\": null, \n \"origin\": \"31.217.26.129\", \n \"url\": \"http://httpbin.org/delete\"\n}\n", 16 | "headers": { 17 | "date": "Wed, 28 Jul 2021 19:21:35 GMT", 18 | "content-type": "application/json", 19 | "content-length": "297", 20 | "connection": "keep-alive", 21 | "server": "gunicorn/19.9.0", 22 | "access-control-allow-origin": "*", 23 | "access-control-allow-credentials": "true" 24 | }, 25 | "status_code": 200, 26 | "type": "ok" 27 | } 28 | } 29 | ] -------------------------------------------------------------------------------- /fixture/vcr_cassettes/finch_get_localhost.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "body": "", 5 | "headers": [], 6 | "method": "get", 7 | "options": [], 8 | "request_body": "", 9 | "url": "http://localhost:34008/server" 10 | }, 11 | "response": { 12 | "binary": false, 13 | "body": "test_response", 14 | "headers": { 15 | "server": "Cowboy", 16 | "date": "Wed, 28 Jul 2021 19:21:32 GMT", 17 | "content-length": "13" 18 | }, 19 | "status_code": 200, 20 | "type": "ok" 21 | } 22 | } 23 | ] -------------------------------------------------------------------------------- /fixture/vcr_cassettes/finch_get_timeout.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "body": "", 5 | "headers": [], 6 | "method": "get", 7 | "options": { 8 | "receive_timeout": 1 9 | }, 10 | "request_body": "", 11 | "url": "http://example.com/" 12 | }, 13 | "response": { 14 | "binary": false, 15 | "body": "%Mint.TransportError{reason: :timeout}", 16 | "headers": [], 17 | "status_code": null, 18 | "type": "error" 19 | } 20 | } 21 | ] -------------------------------------------------------------------------------- /fixture/vcr_cassettes/finch_patch.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "body": "test", 5 | "headers": [], 6 | "method": "patch", 7 | "options": [], 8 | "request_body": "", 9 | "url": "http://httpbin.org/patch" 10 | }, 11 | "response": { 12 | "binary": false, 13 | "body": "{\n \"args\": {}, \n \"data\": \"test\", \n \"files\": {}, \n \"form\": {}, \n \"headers\": {\n \"Content-Length\": \"4\", \n \"Host\": \"httpbin.org\", \n \"User-Agent\": \"mint/1.3.0\", \n \"X-Amzn-Trace-Id\": \"Root=1-6101ae3f-13a1177e3735b9bd36857aa9\"\n }, \n \"json\": null, \n \"origin\": \"31.217.26.129\", \n \"url\": \"http://httpbin.org/patch\"\n}\n", 14 | "headers": { 15 | "date": "Wed, 28 Jul 2021 19:21:35 GMT", 16 | "content-type": "application/json", 17 | "content-length": "328", 18 | "connection": "keep-alive", 19 | "server": "gunicorn/19.9.0", 20 | "access-control-allow-origin": "*", 21 | "access-control-allow-credentials": "true" 22 | }, 23 | "status_code": 200, 24 | "type": "ok" 25 | } 26 | } 27 | ] -------------------------------------------------------------------------------- /fixture/vcr_cassettes/finch_post.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "body": "test", 5 | "headers": [], 6 | "method": "post", 7 | "options": [], 8 | "request_body": "", 9 | "url": "http://httpbin.org/post" 10 | }, 11 | "response": { 12 | "binary": false, 13 | "body": "{\n \"args\": {}, \n \"data\": \"test\", \n \"files\": {}, \n \"form\": {}, \n \"headers\": {\n \"Content-Length\": \"4\", \n \"Host\": \"httpbin.org\", \n \"User-Agent\": \"mint/1.3.0\", \n \"X-Amzn-Trace-Id\": \"Root=1-6101ae3e-4a7c047e3875c515671fa9a9\"\n }, \n \"json\": null, \n \"origin\": \"31.217.26.129\", \n \"url\": \"http://httpbin.org/post\"\n}\n", 14 | "headers": { 15 | "date": "Wed, 28 Jul 2021 19:21:34 GMT", 16 | "content-type": "application/json", 17 | "content-length": "327", 18 | "connection": "keep-alive", 19 | "server": "gunicorn/19.9.0", 20 | "access-control-allow-origin": "*", 21 | "access-control-allow-credentials": "true" 22 | }, 23 | "status_code": 200, 24 | "type": "ok" 25 | } 26 | } 27 | ] -------------------------------------------------------------------------------- /fixture/vcr_cassettes/finch_post_map.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "body": "{\"address\":{\"city\":\"Los Angeles\",\"state\":\"CA\",\"street\":\"123 Main St\",\"zip\":\"90001\"},\"age\":30,\"city\":\"New York\",\"country\":\"USA\",\"favoriteColor\":\"blue\",\"hobbies\":[\"reading\",\"traveling\",\"swimming\"],\"isMarried\":true,\"name\":\"John\",\"phoneNumbers\":[{\"number\":\"555-555-1234\",\"type\":\"home\"},{\"number\":\"555-555-5678\",\"type\":\"work\"}]}", 5 | "headers": [], 6 | "method": "post", 7 | "options": [], 8 | "request_body": "", 9 | "url": "http://httpbin.org/post" 10 | }, 11 | "response": { 12 | "binary": false, 13 | "body": "{\n \"args\": {}, \n \"data\": \"{\\\"address\\\":{\\\"city\\\":\\\"Los Angeles\\\",\\\"state\\\":\\\"CA\\\",\\\"street\\\":\\\"123 Main St\\\",\\\"zip\\\":\\\"90001\\\"},\\\"age\\\":30,\\\"city\\\":\\\"New York\\\",\\\"country\\\":\\\"USA\\\",\\\"favoriteColor\\\":\\\"blue\\\",\\\"hobbies\\\":[\\\"reading\\\",\\\"traveling\\\",\\\"swimming\\\"],\\\"isMarried\\\":true,\\\"name\\\":\\\"John\\\",\\\"phoneNumbers\\\":[{\\\"number\\\":\\\"555-555-1234\\\",\\\"type\\\":\\\"home\\\"},{\\\"number\\\":\\\"555-555-5678\\\",\\\"type\\\":\\\"work\\\"}]}\", \n \"files\": {}, \n \"form\": {}, \n \"headers\": {\n \"Content-Length\": \"323\", \n \"Host\": \"httpbin.org\", \n \"User-Agent\": \"mint/1.5.1\", \n \"X-Amzn-Trace-Id\": \"Root=1-6502b298-152ed17e42e07eca56f829aa\"\n }, \n \"json\": {\n \"address\": {\n \"city\": \"Los Angeles\", \n \"state\": \"CA\", \n \"street\": \"123 Main St\", \n \"zip\": \"90001\"\n }, \n \"age\": 30, \n \"city\": \"New York\", \n \"country\": \"USA\", \n \"favoriteColor\": \"blue\", \n \"hobbies\": [\n \"reading\", \n \"traveling\", \n \"swimming\"\n ], \n \"isMarried\": true, \n \"name\": \"John\", \n \"phoneNumbers\": [\n {\n \"number\": \"555-555-1234\", \n \"type\": \"home\"\n }, \n {\n \"number\": \"555-555-5678\", \n \"type\": \"work\"\n }\n ]\n }, \n \"origin\": \"62.178.80.139\", \n \"url\": \"http://httpbin.org/post\"\n}\n", 14 | "headers": { 15 | "date": "Thu, 14 Sep 2023 07:13:28 GMT", 16 | "content-type": "application/json", 17 | "content-length": "1240", 18 | "connection": "keep-alive", 19 | "server": "gunicorn/19.9.0", 20 | "access-control-allow-origin": "*", 21 | "access-control-allow-credentials": "true" 22 | }, 23 | "status_code": 200, 24 | "type": "ok" 25 | } 26 | } 27 | ] -------------------------------------------------------------------------------- /fixture/vcr_cassettes/finch_put.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "body": "test", 5 | "headers": [], 6 | "method": "put", 7 | "options": { 8 | "receive_timeout": 10000 9 | }, 10 | "request_body": "", 11 | "url": "http://httpbin.org/put" 12 | }, 13 | "response": { 14 | "binary": false, 15 | "body": "{\n \"args\": {}, \n \"data\": \"test\", \n \"files\": {}, \n \"form\": {}, \n \"headers\": {\n \"Content-Length\": \"4\", \n \"Host\": \"httpbin.org\", \n \"User-Agent\": \"mint/1.3.0\", \n \"X-Amzn-Trace-Id\": \"Root=1-6101ae3e-0f8797252c9f3f2368700085\"\n }, \n \"json\": null, \n \"origin\": \"31.217.26.129\", \n \"url\": \"http://httpbin.org/put\"\n}\n", 16 | "headers": { 17 | "date": "Wed, 28 Jul 2021 19:21:34 GMT", 18 | "content-type": "application/json", 19 | "content-length": "326", 20 | "connection": "keep-alive", 21 | "server": "gunicorn/19.9.0", 22 | "access-control-allow-origin": "*", 23 | "access-control-allow-credentials": "true" 24 | }, 25 | "status_code": 200, 26 | "type": "ok" 27 | } 28 | } 29 | ] -------------------------------------------------------------------------------- /fixture/vcr_cassettes/hackney_get.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "body": "", 5 | "headers": [], 6 | "method": "get", 7 | "options": [], 8 | "url": "http://www.example.com" 9 | }, 10 | "response": { 11 | "body": "\n\n\n Example Domain\n\n \n \n \n \n\n\n\n
\n

Example Domain

\n

This domain is established to be used for illustrative examples in documents. You may use this\n domain in examples without prior coordination or asking for permission.

\n

More information...

\n
\n\n\n", 12 | "headers": { 13 | "Accept-Ranges": "bytes", 14 | "Cache-Control": "max-age=604800", 15 | "Content-Type": "text/html", 16 | "Date": "Sun, 30 Nov 2014 11:14:54 GMT", 17 | "Etag": "\"359670651\"", 18 | "Expires": "Sun, 07 Dec 2014 11:14:54 GMT", 19 | "Last-Modified": "Fri, 09 Aug 2013 23:54:35 GMT", 20 | "Server": "ECS (cpm/F9FC)", 21 | "X-Cache": "HIT", 22 | "x-ec-custom-error": "1", 23 | "Content-Length": "1270" 24 | }, 25 | "status_code": 200, 26 | "type": "ok" 27 | } 28 | } 29 | ] -------------------------------------------------------------------------------- /fixture/vcr_cassettes/hackney_get_gzipped.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "body": "", 5 | "headers": { 6 | "Accept-Encoding": "gzip, deflate" 7 | }, 8 | "method": "get", 9 | "options": [], 10 | "request_body": "", 11 | "url": "http://www.example.com" 12 | }, 13 | "response": { 14 | "binary": true, 15 | "body": "g20AAAJeH4sIADuBBVIAA41UQa/TMAy+71eYcgFpXfeAB1PXViBA4gIc4MIxa9zVWpOUJO02offfcdu9ruXtQCu1jh1//mzHSZ5Jk/tzjVB6VWWL5PGHQmYL4Cfx5CvMPp+EqiuET0YJ0kk0aBfDFoVeQF4K69CnQeOLcBNAlE2Mpfd1iL8batPgo9EetQ+7sAHkwyoNPJ581IXfjlC3kLRQmAYt4bE21k/8jyR9mUpsKcewXyyBNHkSVehyUWF6d4Vy/szJdAwugXPngsG2M/IMf3qxX4r8sLem0TLMTWVsDM+LNb+vtuMWJeyedAzrq6oWUpLez3QFMw0Loag6xxB8r1HDD6FdsITgC1YtesoFfMMGWTMqlvDBcgZLcLw1dGipuCL2wkP/ldROSPfpx/B2va5PT3neowLReHOD7v3M4VbuxST+zliJNrRCUuNiuEO1nVAScUX6sOR/S448ygnBR7jXmzebzQSx60UoMTdWeDLMVRuNU9D3CiUJeKHEKbxk+a7L8uW0ZfMO/k8mD6M0L+SkmPOKzfp+w/ZPadZz61jvsWRXEsM3ifojmnXyIomGeVwkXWo8nkzycpDLuyejyarBVmc/S3Igez2whM6LXUWu5F54AzuExrFYGAtUVY3zXdVbBBwQHc8Pe+eN4gFzK/hlGs753DmBZ+Th4F3Q9dXrSL40jYfaEiPnhktBuu8n8Fq4A6feB63RKnKODaskqkfWCd8XFos06G6NOIqOx+OKhBYrY/fREM9Fl2hB9tVY5PCMp/oYqxWDiawHTKK+Ukl0qVs0XG9/AQiVqov2BAAA", 16 | "headers": { 17 | "Content-Encoding": "gzip", 18 | "Cache-Control": "max-age=604800", 19 | "Content-Type": "text/html", 20 | "Date": "Wed, 13 Sep 2017 08:22:45 GMT", 21 | "Etag": "\"359670651+gzip\"", 22 | "Expires": "Wed, 20 Sep 2017 08:22:45 GMT", 23 | "Last-Modified": "Fri, 09 Aug 2013 23:54:35 GMT", 24 | "Server": "ECS (lga/1378)", 25 | "Vary": "Accept-Encoding", 26 | "X-Cache": "HIT", 27 | "Content-Length": "606" 28 | }, 29 | "status_code": 200, 30 | "type": "ok" 31 | } 32 | } 33 | ] -------------------------------------------------------------------------------- /fixture/vcr_cassettes/hackney_get_localhost.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "body": "", 5 | "headers": [], 6 | "method": "get", 7 | "options": { 8 | "with_body": "true" 9 | }, 10 | "request_body": "", 11 | "url": "http://localhost:34009/server" 12 | }, 13 | "response": { 14 | "binary": false, 15 | "body": "test_response", 16 | "headers": { 17 | "server": "Cowboy", 18 | "date": "Tue, 29 Sep 2020 11:50:14 GMT", 19 | "content-length": "13" 20 | }, 21 | "status_code": 200, 22 | "type": "ok" 23 | } 24 | } 25 | ] -------------------------------------------------------------------------------- /fixture/vcr_cassettes/hackney_head.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "body": "", 5 | "headers": [], 6 | "method": "head", 7 | "options": [], 8 | "request_body": "", 9 | "url": "http://www.example.com" 10 | }, 11 | "response": { 12 | "body": null, 13 | "headers": { 14 | "Content-Encoding": "gzip", 15 | "Accept-Ranges": "bytes", 16 | "Cache-Control": "max-age=604800", 17 | "Content-Type": "text/html", 18 | "Date": "Fri, 27 Jan 2017 12:44:46 GMT", 19 | "Etag": "\"359670651+gzip\"", 20 | "Expires": "Fri, 03 Feb 2017 12:44:46 GMT", 21 | "Last-Modified": "Fri, 09 Aug 2013 23:54:35 GMT", 22 | "Server": "ECS (ewr/15BD)", 23 | "X-Cache": "HIT", 24 | "x-ec-custom-error": "1", 25 | "Content-Length": "606" 26 | }, 27 | "status_code": 200, 28 | "type": "ok" 29 | } 30 | } 31 | ] -------------------------------------------------------------------------------- /fixture/vcr_cassettes/hackney_invalid_client.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "body": "", 5 | "headers": [], 6 | "method": "get", 7 | "options": [], 8 | "url": "http://www.example.com" 9 | }, 10 | "response": { 11 | "body": "\n\n\n Example Domain\n\n \n \n \n \n\n\n\n
\n

Example Domain

\n

This domain is established to be used for illustrative examples in documents. You may use this\n domain in examples without prior coordination or asking for permission.

\n

More information...

\n
\n\n\n", 12 | "headers": { 13 | "Accept-Ranges": "bytes", 14 | "Cache-Control": "max-age=604800", 15 | "Content-Type": "text/html", 16 | "Date": "Sun, 30 Nov 2014 11:14:55 GMT", 17 | "Etag": "\"359670651\"", 18 | "Expires": "Sun, 07 Dec 2014 11:14:55 GMT", 19 | "Last-Modified": "Fri, 09 Aug 2013 23:54:35 GMT", 20 | "Server": "ECS (cpm/F9FC)", 21 | "X-Cache": "HIT", 22 | "x-ec-custom-error": "1", 23 | "Content-Length": "1270" 24 | }, 25 | "status_code": 200, 26 | "type": "ok" 27 | } 28 | } 29 | ] -------------------------------------------------------------------------------- /fixture/vcr_cassettes/hackney_path_encode_fun.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "body": "", 5 | "headers": [], 6 | "method": "get", 7 | "options": [], 8 | "request_body": "", 9 | "url": "http://www.example.com" 10 | }, 11 | "response": { 12 | "body": "\n\n\n Example Domain\n\n \n \n \n \n\n\n\n
\n

Example Domain

\n

This domain is established to be used for illustrative examples in documents. You may use this\n domain in examples without prior coordination or asking for permission.

\n

More information...

\n
\n\n\n", 13 | "headers": { 14 | "Cache-Control": "max-age=604800", 15 | "Content-Type": "text/html", 16 | "Date": "Mon, 21 Aug 2017 12:58:50 GMT", 17 | "Etag": "\"359670651+ident\"", 18 | "Expires": "Mon, 28 Aug 2017 12:58:50 GMT", 19 | "Last-Modified": "Fri, 09 Aug 2013 23:54:35 GMT", 20 | "Server": "ECS (rhv/818F)", 21 | "Vary": "Accept-Encoding", 22 | "X-Cache": "HIT", 23 | "Content-Length": "1270" 24 | }, 25 | "status_code": 200, 26 | "type": "ok" 27 | } 28 | } 29 | ] -------------------------------------------------------------------------------- /fixture/vcr_cassettes/hackney_with_body.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "body": "", 5 | "headers": [], 6 | "method": "get", 7 | "options": { 8 | "with_body": true 9 | }, 10 | "request_body": "", 11 | "url": "http://www.example.com" 12 | }, 13 | "response": { 14 | "body": "\"\\n\\n\\n Example Domain\\n\\n \\n \\n \\n \\n\\n\\n\\n
\\n

Example Domain

\\n

This domain is established to be used for illustrative examples in documents. You may use this\\n domain in examples without prior coordination or asking for permission.

\\n

More information...

\\n
\\n\\n\\n\"", 15 | "headers": { 16 | "Cache-Control": "max-age=604800", 17 | "Content-Type": "text/html", 18 | "Date": "Tue, 01 Nov 2016 04:58:34 GMT", 19 | "Etag": "\"359670651+ident\"", 20 | "Expires": "Tue, 08 Nov 2016 04:58:34 GMT", 21 | "Last-Modified": "Fri, 09 Aug 2013 23:54:35 GMT", 22 | "Server": "ECS (pae/3796)", 23 | "Vary": "Accept-Encoding", 24 | "X-Cache": "HIT", 25 | "x-ec-custom-error": "1", 26 | "Content-Length": "1270" 27 | }, 28 | "status_code": 200, 29 | "type": "ok" 30 | } 31 | } 32 | ] -------------------------------------------------------------------------------- /fixture/vcr_cassettes/httpc_get_localhost.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "body": "", 5 | "headers": [], 6 | "method": "get", 7 | "options": { 8 | "httpc_options": [], 9 | "http_options": [] 10 | }, 11 | "request_body": "", 12 | "url": "http://localhost:34010/server" 13 | }, 14 | "response": { 15 | "binary": false, 16 | "body": "test_response", 17 | "headers": { 18 | "date": "Tue, 29 Sep 2020 11:52:26 GMT", 19 | "server": "Cowboy", 20 | "content-length": "13" 21 | }, 22 | "status_code": [ 23 | "HTTP/1.1", 24 | 200, 25 | "OK" 26 | ], 27 | "type": "ok" 28 | } 29 | } 30 | ] -------------------------------------------------------------------------------- /fixture/vcr_cassettes/httpoison_delete.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "body": "", 5 | "headers": [], 6 | "method": "delete", 7 | "options": { 8 | "connect_timeout": 5000 9 | }, 10 | "url": "http://httpbin.org/delete" 11 | }, 12 | "response": { 13 | "body": "{\n \"args\": {}, \n \"data\": \"\", \n \"files\": {}, \n \"form\": {}, \n \"headers\": {\n \"Connect-Time\": \"0\", \n \"Connection\": \"close\", \n \"Content-Length\": \"0\", \n \"Host\": \"httpbin.org\", \n \"Total-Route-Time\": \"0\", \n \"User-Agent\": \"hackney/0.14.3\", \n \"Via\": \"1.1 vegur\", \n \"X-Request-Id\": \"83104f25-7b28-46e0-85a2-71c18cec1d59\"\n }, \n \"json\": null, \n \"origin\": \"219.52.146.216\", \n \"url\": \"http://httpbin.org/delete\"\n}", 14 | "headers": { 15 | "Connection": "keep-alive", 16 | "Server": "gunicorn/18.0", 17 | "Date": "Sun, 30 Nov 2014 11:14:57 GMT", 18 | "Content-Type": "application/json", 19 | "Content-Length": "431", 20 | "Access-Control-Allow-Origin": "*", 21 | "Access-Control-Allow-Credentials": "true", 22 | "Via": "1.1 vegur" 23 | }, 24 | "status_code": 200, 25 | "type": "ok" 26 | } 27 | } 28 | ] -------------------------------------------------------------------------------- /fixture/vcr_cassettes/httpoison_get.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "body": "", 5 | "headers": [], 6 | "method": "get", 7 | "options": { 8 | "connect_timeout": 5000 9 | }, 10 | "url": "http://example.com" 11 | }, 12 | "response": { 13 | "body": "\n\n\n Example Domain\n\n \n \n \n \n\n\n\n
\n

Example Domain

\n

This domain is established to be used for illustrative examples in documents. You may use this\n domain in examples without prior coordination or asking for permission.

\n

More information...

\n
\n\n\n", 14 | "headers": { 15 | "Accept-Ranges": "bytes", 16 | "Cache-Control": "max-age=604800", 17 | "Content-Type": "text/html", 18 | "Date": "Sun, 30 Nov 2014 11:14:55 GMT", 19 | "Etag": "\"359670651\"", 20 | "Expires": "Sun, 07 Dec 2014 11:14:55 GMT", 21 | "Last-Modified": "Fri, 09 Aug 2013 23:54:35 GMT", 22 | "Server": "ECS (cpm/F9FC)", 23 | "X-Cache": "HIT", 24 | "x-ec-custom-error": "1", 25 | "Content-Length": "1270" 26 | }, 27 | "status_code": 200, 28 | "type": "ok" 29 | } 30 | } 31 | ] -------------------------------------------------------------------------------- /fixture/vcr_cassettes/httpoison_get_basic_auth.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "body": "", 5 | "headers": [], 6 | "method": "get", 7 | "options": { 8 | "connect_timeout": 5000, 9 | "recv_timeout": "infinity", 10 | "basic_auth": [ 11 | "user", 12 | "password" 13 | ] 14 | }, 15 | "request_body": "", 16 | "url": "http://example.com" 17 | }, 18 | "response": { 19 | "body": "\n\n\n Example Domain\n\n \n \n \n \n\n\n\n
\n

Example Domain

\n

This domain is established to be used for illustrative examples in documents. You may use this\n domain in examples without prior coordination or asking for permission.

\n

More information...

\n
\n\n\n", 20 | "headers": { 21 | "Accept-Ranges": "bytes", 22 | "Cache-Control": "max-age=604800", 23 | "Content-Type": "text/html", 24 | "Date": "Sun, 08 Nov 2015 03:58:57 GMT", 25 | "Etag": "\"359670651\"", 26 | "Expires": "Sun, 15 Nov 2015 03:58:57 GMT", 27 | "Last-Modified": "Fri, 09 Aug 2013 23:54:35 GMT", 28 | "Server": "ECS (cpm/F9D5)", 29 | "X-Cache": "HIT", 30 | "x-ec-custom-error": "1", 31 | "Content-Length": "1270" 32 | }, 33 | "status_code": 200, 34 | "type": "ok" 35 | } 36 | } 37 | ] -------------------------------------------------------------------------------- /fixture/vcr_cassettes/httpoison_get_error.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "body": "", 5 | "headers": [], 6 | "method": "get", 7 | "options": { 8 | "connect_timeout": 5000 9 | }, 10 | "url": "http://invalid_url" 11 | }, 12 | "response": { 13 | "body": "nxdomain", 14 | "headers": [], 15 | "status_code": null, 16 | "type": "error" 17 | } 18 | } 19 | ] -------------------------------------------------------------------------------- /fixture/vcr_cassettes/httpoison_head.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "body": "", 5 | "headers": [], 6 | "method": "head", 7 | "options": [], 8 | "request_body": "", 9 | "url": "http://example.com" 10 | }, 11 | "response": { 12 | "body": null, 13 | "headers": { 14 | "Content-Encoding": "gzip", 15 | "Accept-Ranges": "bytes", 16 | "Cache-Control": "max-age=604800", 17 | "Content-Type": "text/html", 18 | "Date": "Fri, 27 Jan 2017 12:44:45 GMT", 19 | "Etag": "\"359670651+gzip\"", 20 | "Expires": "Fri, 03 Feb 2017 12:44:45 GMT", 21 | "Last-Modified": "Fri, 09 Aug 2013 23:54:35 GMT", 22 | "Server": "ECS (ewr/15BD)", 23 | "X-Cache": "HIT", 24 | "x-ec-custom-error": "1", 25 | "Content-Length": "606" 26 | }, 27 | "status_code": 200, 28 | "type": "ok" 29 | } 30 | } 31 | ] -------------------------------------------------------------------------------- /fixture/vcr_cassettes/httpoison_mutipart_post.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "body": "{:multipart, [{:file, \"tmp/vcr_tmp/dummy_file.txt\", {[\"form-data\"], [name: \"\\\"photo\\\"\", filename: \"\\\"dummy_file.txt\\\"\"]}, []}]}", 5 | "headers": [], 6 | "method": "post", 7 | "options": { 8 | "recv_timeout": 30000 9 | }, 10 | "request_body": "", 11 | "url": "https://httpbin.org/post" 12 | }, 13 | "response": { 14 | "body": "{\n \"args\": {}, \n \"data\": \"\", \n \"files\": {\n \"photo\": \"dummy_file\"\n }, \n \"form\": {}, \n \"headers\": {\n \"Content-Length\": \"227\", \n \"Content-Type\": \"multipart/form-data; boundary=---------------------------mtynipxrmpegseog\", \n \"Host\": \"httpbin.org\", \n \"User-Agent\": \"hackney/1.4.4\"\n }, \n \"json\": null, \n \"origin\": \"219.52.146.216\", \n \"url\": \"https://httpbin.org/post\"\n}\n", 15 | "headers": { 16 | "Server": "nginx", 17 | "Date": "Sun, 13 Dec 2015 09:49:55 GMT", 18 | "Content-Type": "application/json", 19 | "Content-Length": "389", 20 | "Connection": "keep-alive", 21 | "Access-Control-Allow-Origin": "*", 22 | "Access-Control-Allow-Credentials": "true" 23 | }, 24 | "status_code": 200, 25 | "type": "ok" 26 | } 27 | } 28 | ] -------------------------------------------------------------------------------- /fixture/vcr_cassettes/httpoison_patch.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "body": "test", 5 | "headers": [], 6 | "method": "patch", 7 | "options": { 8 | "connect_timeout": 5000 9 | }, 10 | "url": "http://httpbin.org/patch" 11 | }, 12 | "response": { 13 | "body": "{\n \"args\": {}, \n \"data\": \"test\", \n \"files\": {}, \n \"form\": {}, \n \"headers\": {\n \"Connect-Time\": \"1\", \n \"Connection\": \"close\", \n \"Content-Length\": \"4\", \n \"Content-Type\": \"application/octet-stream\", \n \"Host\": \"httpbin.org\", \n \"Total-Route-Time\": \"0\", \n \"User-Agent\": \"hackney/0.14.3\", \n \"Via\": \"1.1 vegur\", \n \"X-Request-Id\": \"5fd4064f-91de-4e0f-a3a4-890dbfe266ae\"\n }, \n \"json\": null, \n \"origin\": \"219.52.146.216\", \n \"url\": \"http://httpbin.org/patch\"\n}", 14 | "headers": { 15 | "Connection": "keep-alive", 16 | "Server": "gunicorn/18.0", 17 | "Date": "Sun, 30 Nov 2014 11:14:56 GMT", 18 | "Content-Type": "application/json", 19 | "Content-Length": "483", 20 | "Access-Control-Allow-Origin": "*", 21 | "Access-Control-Allow-Credentials": "true", 22 | "Via": "1.1 vegur" 23 | }, 24 | "status_code": 200, 25 | "type": "ok" 26 | } 27 | } 28 | ] -------------------------------------------------------------------------------- /fixture/vcr_cassettes/httpoison_post.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "body": "test", 5 | "headers": [], 6 | "method": "post", 7 | "options": { 8 | "connect_timeout": 5000 9 | }, 10 | "url": "http://httpbin.org/post" 11 | }, 12 | "response": { 13 | "body": "{\n \"args\": {}, \n \"data\": \"test\", \n \"files\": {}, \n \"form\": {}, \n \"headers\": {\n \"Connect-Time\": \"5\", \n \"Connection\": \"close\", \n \"Content-Length\": \"4\", \n \"Content-Type\": \"application/octet-stream\", \n \"Host\": \"httpbin.org\", \n \"Total-Route-Time\": \"0\", \n \"User-Agent\": \"hackney/0.14.3\", \n \"Via\": \"1.1 vegur\", \n \"X-Request-Id\": \"2ebb1462-bcf2-46a7-b6c8-d0fd2b88b17c\"\n }, \n \"json\": null, \n \"origin\": \"219.52.146.216\", \n \"url\": \"http://httpbin.org/post\"\n}", 14 | "headers": { 15 | "Connection": "keep-alive", 16 | "Server": "gunicorn/18.0", 17 | "Date": "Sun, 30 Nov 2014 11:14:53 GMT", 18 | "Content-Type": "application/json", 19 | "Content-Length": "482", 20 | "Access-Control-Allow-Origin": "*", 21 | "Access-Control-Allow-Credentials": "true", 22 | "Via": "1.1 vegur" 23 | }, 24 | "status_code": 200, 25 | "type": "ok" 26 | } 27 | } 28 | ] -------------------------------------------------------------------------------- /fixture/vcr_cassettes/httpoison_post_form.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "body": "key=value", 5 | "headers": { 6 | "Content-type": "application/x-www-form-urlencoded" 7 | }, 8 | "method": "post", 9 | "options": { 10 | "connect_timeout": 5000 11 | }, 12 | "url": "http://httpbin.org/post" 13 | }, 14 | "response": { 15 | "body": "{\n \"args\": {}, \n \"data\": \"\", \n \"files\": {}, \n \"form\": {\n \"key\": \"value\"\n }, \n \"headers\": {\n \"Content-Length\": \"9\", \n \"Content-Type\": \"application/x-www-form-urlencoded; charset=utf-8\", \n \"Host\": \"httpbin.org\", \n \"User-Agent\": \"hackney/0.14.3\"\n }, \n \"json\": null, \n \"origin\": \"219.52.146.216\", \n \"url\": \"http://httpbin.org/post\"\n}\n", 16 | "headers": { 17 | "Server": "nginx", 18 | "Date": "Wed, 22 Jul 2015 13:39:08 GMT", 19 | "Content-Type": "application/json", 20 | "Content-Length": "355", 21 | "Connection": "keep-alive", 22 | "Access-Control-Allow-Origin": "*", 23 | "Access-Control-Allow-Credentials": "true" 24 | }, 25 | "status_code": 200, 26 | "type": "ok" 27 | } 28 | } 29 | ] -------------------------------------------------------------------------------- /fixture/vcr_cassettes/httpoison_post_ssl.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "body": "", 5 | "headers": [], 6 | "method": "post", 7 | "options": { 8 | "ssl_options": { 9 | "versions": [ 10 | "tlsv1.2" 11 | ] 12 | } 13 | }, 14 | "request_body": "", 15 | "url": "https://example.com" 16 | }, 17 | "response": { 18 | "body": "\n\n\n Example Domain\n\n \n \n \n \n\n\n\n
\n

Example Domain

\n

This domain is established to be used for illustrative examples in documents. You may use this\n domain in examples without prior coordination or asking for permission.

\n

More information...

\n
\n\n\n", 19 | "headers": { 20 | "Accept-Ranges": "bytes", 21 | "Cache-Control": "max-age=604800", 22 | "Content-Type": "text/html", 23 | "Date": "Sun, 04 Jun 2017 15:03:55 GMT", 24 | "Etag": "\"359670651\"", 25 | "Expires": "Sun, 11 Jun 2017 15:03:55 GMT", 26 | "Last-Modified": "Fri, 09 Aug 2013 23:54:35 GMT", 27 | "Server": "EOS (lax004/2811)", 28 | "Content-Length": "1270" 29 | }, 30 | "status_code": 200, 31 | "type": "ok" 32 | } 33 | } 34 | ] -------------------------------------------------------------------------------- /fixture/vcr_cassettes/httpoison_put.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "body": "test", 5 | "headers": [], 6 | "method": "put", 7 | "options": { 8 | "connect_timeout": 5000 9 | }, 10 | "url": "http://httpbin.org/put" 11 | }, 12 | "response": { 13 | "body": "{\n \"args\": {}, \n \"data\": \"test\", \n \"files\": {}, \n \"form\": {}, \n \"headers\": {\n \"Connect-Time\": \"0\", \n \"Connection\": \"close\", \n \"Content-Length\": \"4\", \n \"Content-Type\": \"application/octet-stream\", \n \"Host\": \"httpbin.org\", \n \"Total-Route-Time\": \"0\", \n \"User-Agent\": \"hackney/0.14.3\", \n \"Via\": \"1.1 vegur\", \n \"X-Request-Id\": \"e8445266-8f23-4713-8d54-7ebe2485fe47\"\n }, \n \"json\": null, \n \"origin\": \"219.52.146.216\", \n \"url\": \"http://httpbin.org/put\"\n}", 14 | "headers": { 15 | "Connection": "keep-alive", 16 | "Server": "gunicorn/18.0", 17 | "Date": "Sun, 30 Nov 2014 11:14:56 GMT", 18 | "Content-Type": "application/json", 19 | "Content-Length": "481", 20 | "Access-Control-Allow-Origin": "*", 21 | "Access-Control-Allow-Credentials": "true", 22 | "Via": "1.1 vegur" 23 | }, 24 | "status_code": 200, 25 | "type": "ok" 26 | } 27 | } 28 | ] -------------------------------------------------------------------------------- /fixture/vcr_cassettes/ibrowse_get_localhost.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "body": "", 5 | "headers": [], 6 | "method": "get", 7 | "options": [], 8 | "request_body": "", 9 | "url": "http://localhost:34011/server" 10 | }, 11 | "response": { 12 | "binary": false, 13 | "body": "test_response", 14 | "headers": { 15 | "server": "Cowboy", 16 | "date": "Tue, 29 Sep 2020 11:56:58 GMT", 17 | "content-length": "13" 18 | }, 19 | "status_code": 200, 20 | "type": "ok" 21 | } 22 | } 23 | ] -------------------------------------------------------------------------------- /fixture/vcr_cassettes/ignore_localhost_on.json: -------------------------------------------------------------------------------- 1 | [] -------------------------------------------------------------------------------- /fixture/vcr_cassettes/ignore_localhost_unset.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "body": "", 5 | "headers": [], 6 | "method": "get", 7 | "options": [], 8 | "request_body": "", 9 | "url": "http://localhost:34012/server" 10 | }, 11 | "response": { 12 | "binary": false, 13 | "body": "test_response_before", 14 | "headers": { 15 | "server": "Cowboy", 16 | "date": "Sun, 08 Apr 2018 12:50:46 GMT", 17 | "content-length": "20" 18 | }, 19 | "status_code": 200, 20 | "type": "ok" 21 | } 22 | } 23 | ] -------------------------------------------------------------------------------- /fixture/vcr_cassettes/ignore_localhost_with_headers.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "body": "", 5 | "headers": { 6 | "User-Agent": "ExVCR" 7 | }, 8 | "method": "get", 9 | "options": [], 10 | "request_body": "", 11 | "url": "http://127.0.0.1:34012/server" 12 | }, 13 | "response": { 14 | "binary": false, 15 | "body": "test_response_before", 16 | "headers": { 17 | "server": "Cowboy", 18 | "date": "Wed, 05 Sep 2018 12:05:22 GMT", 19 | "content-length": "20" 20 | }, 21 | "status_code": 200, 22 | "type": "ok" 23 | } 24 | } 25 | ] -------------------------------------------------------------------------------- /fixture/vcr_cassettes/ignore_urls_on.json: -------------------------------------------------------------------------------- 1 | [] -------------------------------------------------------------------------------- /fixture/vcr_cassettes/ignore_urls_unset.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "body": "", 5 | "headers": [], 6 | "method": "get", 7 | "options": [], 8 | "request_body": "", 9 | "url": "http://localhost:34013/server" 10 | }, 11 | "response": { 12 | "binary": false, 13 | "body": "test_response_before", 14 | "headers": { 15 | "server": "Cowboy", 16 | "date": "Sun, 08 Apr 2018 12:50:46 GMT", 17 | "content-length": "20" 18 | }, 19 | "status_code": 200, 20 | "type": "ok" 21 | } 22 | }, 23 | { 24 | "request": { 25 | "body": "", 26 | "headers": [], 27 | "method": "get", 28 | "options": [], 29 | "request_body": "", 30 | "url": "http://127.0.0.1:34013/server" 31 | }, 32 | "response": { 33 | "binary": false, 34 | "body": "test_response_before", 35 | "headers": { 36 | "server": "Cowboy", 37 | "date": "Sun, 08 Apr 2018 12:50:46 GMT", 38 | "content-length": "20" 39 | }, 40 | "status_code": 200, 41 | "type": "ok" 42 | } 43 | } 44 | ] -------------------------------------------------------------------------------- /fixture/vcr_cassettes/ignore_urls_with_headers.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "body": "", 5 | "headers": { 6 | "User-Agent": "ExVCR" 7 | }, 8 | "method": "get", 9 | "options": [], 10 | "request_body": "", 11 | "url": "http://127.0.0.1:34006/server" 12 | }, 13 | "response": { 14 | "binary": false, 15 | "body": "test_response_before", 16 | "headers": { 17 | "server": "Cowboy", 18 | "date": "Tue, 20 Apr 2021 13:54:51 GMT", 19 | "content-length": "20" 20 | }, 21 | "status_code": 200, 22 | "type": "ok" 23 | } 24 | } 25 | ] -------------------------------------------------------------------------------- /fixture/vcr_cassettes/option_clean_all.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "body": "", 5 | "headers": [], 6 | "method": "get", 7 | "options": [], 8 | "url": "http://localhost:34003/server" 9 | }, 10 | "response": { 11 | "body": "test_response1", 12 | "headers": { 13 | "connection": "keep-alive", 14 | "server": "Cowboy", 15 | "date": "Sun, 30 Nov 2014 11:14:59 GMT", 16 | "content-length": "14" 17 | }, 18 | "status_code": 200, 19 | "type": "ok" 20 | } 21 | } 22 | ] -------------------------------------------------------------------------------- /fixture/vcr_cassettes/option_clean_each.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "body": "", 5 | "headers": [], 6 | "method": "get", 7 | "options": [], 8 | "url": "http://localhost:34004/server" 9 | }, 10 | "response": { 11 | "body": "test_response1", 12 | "headers": { 13 | "connection": "keep-alive", 14 | "server": "Cowboy", 15 | "date": "Sun, 30 Nov 2014 11:14:52 GMT", 16 | "content-length": "14" 17 | }, 18 | "status_code": 200, 19 | "type": "ok" 20 | } 21 | } 22 | ] -------------------------------------------------------------------------------- /fixture/vcr_cassettes/return_value_from_block.json: -------------------------------------------------------------------------------- 1 | [] -------------------------------------------------------------------------------- /fixture/vcr_cassettes/return_value_from_block_throws_error.json: -------------------------------------------------------------------------------- 1 | [] -------------------------------------------------------------------------------- /fixture/vcr_cassettes/user_defined_matchers_matching.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "body": "p=3", 5 | "headers": { 6 | "User-Agent": "My App", 7 | "X-Special-Header": "Value One" 8 | }, 9 | "method": "post", 10 | "options": [], 11 | "request_body": "", 12 | "url": "http://localhost:34006/server" 13 | }, 14 | "response": { 15 | "binary": false, 16 | "body": "test_response_before", 17 | "headers": { 18 | "server": "Cowboy", 19 | "date": "Fri, 13 Sep 2019 13:02:19 GMT", 20 | "content-length": "20" 21 | }, 22 | "status_code": 200, 23 | "type": "ok" 24 | } 25 | } 26 | ] -------------------------------------------------------------------------------- /fixture/vcr_cassettes/user_defined_matchers_not_matching.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "body": "p=3", 5 | "headers": { 6 | "User-Agent": "My App", 7 | "X-Special-Header": "Value One" 8 | }, 9 | "method": "post", 10 | "options": [], 11 | "request_body": "", 12 | "url": "http://localhost:34006/server" 13 | }, 14 | "response": { 15 | "binary": false, 16 | "body": "test_response_before", 17 | "headers": { 18 | "server": "Cowboy", 19 | "date": "Fri, 13 Sep 2019 13:02:18 GMT", 20 | "content-length": "20" 21 | }, 22 | "status_code": 200, 23 | "type": "ok" 24 | } 25 | }, 26 | { 27 | "request": { 28 | "body": "p=4", 29 | "headers": { 30 | "User-Agent": "Other App", 31 | "X-Special-Header": "Value Two" 32 | }, 33 | "method": "post", 34 | "options": [], 35 | "request_body": "", 36 | "url": "http://localhost:34006/server" 37 | }, 38 | "response": { 39 | "binary": false, 40 | "body": "test_response_after", 41 | "headers": { 42 | "server": "Cowboy", 43 | "date": "Fri, 13 Sep 2019 13:02:18 GMT", 44 | "content-length": "19" 45 | }, 46 | "status_code": 200, 47 | "type": "ok" 48 | } 49 | } 50 | ] -------------------------------------------------------------------------------- /lib/exvcr.ex: -------------------------------------------------------------------------------- 1 | defmodule ExVCR do 2 | @moduledoc """ 3 | Record and replay HTTP interactions library for elixir. 4 | It's inspired by Ruby's VCR, and trying to provide similar functionalities. 5 | """ 6 | end 7 | -------------------------------------------------------------------------------- /lib/exvcr/actor.ex: -------------------------------------------------------------------------------- 1 | defmodule ExVCR.Actor do 2 | @moduledoc """ 3 | Provides data store for values used by ExVCR.Recorder. 4 | """ 5 | 6 | defmodule Responses do 7 | @moduledoc """ 8 | Stores request/response for the recorder. 9 | """ 10 | 11 | use GenServer 12 | 13 | def start(arg) do 14 | GenServer.start(__MODULE__, arg) 15 | end 16 | 17 | def append(pid, x) do 18 | GenServer.cast(pid, {:append, x}) 19 | end 20 | 21 | def set(pid, x) do 22 | GenServer.cast(pid, {:set, x}) 23 | end 24 | 25 | def get(pid) do 26 | GenServer.call(pid, :get) 27 | end 28 | 29 | def update(pid, finder, updater) do 30 | GenServer.call(pid, {:update, finder, updater}) 31 | end 32 | 33 | def pop(pid) do 34 | GenServer.call(pid, :pop) 35 | end 36 | 37 | # Callbacks 38 | 39 | @impl true 40 | def init(arg) do 41 | {:ok, arg} 42 | end 43 | 44 | @impl true 45 | def handle_cast({:append, x}, state) do 46 | {:noreply, [x | state]} 47 | end 48 | 49 | @impl true 50 | def handle_cast({:set, x}, _state) do 51 | {:noreply, x} 52 | end 53 | 54 | @impl true 55 | def handle_call(:get, _from, state) do 56 | {:reply, state, state} 57 | end 58 | 59 | @impl true 60 | def handle_call({:update, finder, updater}, _from, state) do 61 | new_state = 62 | Enum.map(state, fn record -> 63 | if finder.(record) do 64 | updater.(record) 65 | else 66 | record 67 | end 68 | end) 69 | 70 | {:reply, new_state, new_state} 71 | end 72 | 73 | @impl true 74 | def handle_call(:pop, _from, state) do 75 | case state do 76 | [] -> {:reply, state, state} 77 | [head | tail] -> {:reply, head, tail} 78 | end 79 | end 80 | end 81 | 82 | defmodule Options do 83 | @moduledoc """ 84 | Stores option parameters for the recorder. 85 | """ 86 | 87 | use GenServer 88 | 89 | def start(arg) do 90 | GenServer.start(__MODULE__, arg) 91 | end 92 | 93 | def set(pid, x) do 94 | GenServer.cast(pid, {:set, x}) 95 | end 96 | 97 | def get(pid) do 98 | GenServer.call(pid, :get) 99 | end 100 | 101 | # Callbacks 102 | 103 | @impl true 104 | def init(arg) do 105 | {:ok, arg} 106 | end 107 | 108 | @impl true 109 | def handle_cast({:set, x}, _state) do 110 | {:noreply, x} 111 | end 112 | 113 | @impl true 114 | def handle_call(:get, _from, state) do 115 | {:reply, state, state} 116 | end 117 | end 118 | 119 | defmodule CurrentRecorder do 120 | @moduledoc """ 121 | Stores current recorder to be able to fetch it inside of the mocked version of the adapter. 122 | """ 123 | 124 | use GenServer 125 | 126 | def start_link(_) do 127 | GenServer.start_link(__MODULE__, default_state(), name: __MODULE__) 128 | end 129 | 130 | def set(x) do 131 | GenServer.cast(__MODULE__, {:set, x}) 132 | end 133 | 134 | def get do 135 | GenServer.call(__MODULE__, :get) 136 | end 137 | 138 | def default_state(), do: nil 139 | 140 | # Callbacks 141 | 142 | @impl true 143 | def init(state) do 144 | {:ok, state} 145 | end 146 | 147 | @impl true 148 | def handle_cast({:set, x}, _state) do 149 | {:noreply, x} 150 | end 151 | 152 | @impl true 153 | def handle_call(:get, _from, state) do 154 | {:reply, state, state} 155 | end 156 | end 157 | end 158 | -------------------------------------------------------------------------------- /lib/exvcr/adapter.ex: -------------------------------------------------------------------------------- 1 | defmodule ExVCR.Adapter do 2 | @moduledoc """ 3 | Provides helpers for adapters. 4 | """ 5 | 6 | defmacro __using__(_) do 7 | quote do 8 | @doc """ 9 | Returns the name of the mock target module. 10 | """ 11 | def module_name, do: raise(ExVCR.ImplementationMissingError) 12 | defoverridable module_name: 0 13 | 14 | @doc """ 15 | Returns list of the mock target methods with function name and callback. 16 | Implementation for global mock. 17 | """ 18 | def target_methods(), do: raise(ExVCR.ImplementationMissingError) 19 | defoverridable target_methods: 0 20 | 21 | @doc """ 22 | Returns list of the mock target methods with function name and callback. 23 | """ 24 | def target_methods(recorder), do: raise(ExVCR.ImplementationMissingError) 25 | defoverridable target_methods: 1 26 | 27 | @doc """ 28 | Generate key for searching response. 29 | [url: url, method: method] needs to be returned. 30 | """ 31 | def generate_keys_for_request(request), do: raise(ExVCR.ImplementationMissingError) 32 | defoverridable generate_keys_for_request: 1 33 | 34 | @doc """ 35 | Callback from ExVCR.Handler when response is retrieved from the HTTP server. 36 | """ 37 | def hook_response_from_server(response), do: response 38 | defoverridable hook_response_from_server: 1 39 | 40 | @doc """ 41 | Callback from ExVCR.Handler when response is retrieved from the json file cache. 42 | """ 43 | def hook_response_from_cache(_request, response), do: response 44 | defoverridable hook_response_from_cache: 2 45 | 46 | @doc """ 47 | Callback from ExVCR.Handler to get the response content tuple from the ExVCR.Response record. 48 | """ 49 | def get_response_value_from_cache(response) do 50 | if response.type == "error" do 51 | {:error, response.body} 52 | else 53 | {:ok, response.status_code, response.headers, response.body} 54 | end 55 | end 56 | 57 | defoverridable get_response_value_from_cache: 1 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /lib/exvcr/adapter/finch.ex: -------------------------------------------------------------------------------- 1 | if Code.ensure_loaded?(Finch) do 2 | defmodule ExVCR.Adapter.Finch do 3 | @moduledoc """ 4 | Provides adapter methods to mock Finch methods. 5 | """ 6 | 7 | use ExVCR.Adapter 8 | 9 | alias ExVCR.Util 10 | 11 | defmacro __using__(_opts) do 12 | # do nothing 13 | end 14 | 15 | defdelegate convert_from_string(string), to: ExVCR.Adapter.Finch.Converter 16 | defdelegate convert_to_string(request, response), to: ExVCR.Adapter.Finch.Converter 17 | defdelegate parse_request_body(request_body), to: ExVCR.Adapter.Finch.Converter 18 | 19 | @doc """ 20 | Returns the name of the mock target module. 21 | """ 22 | def module_name do 23 | Finch 24 | end 25 | 26 | @doc """ 27 | Returns list of the mock target methods with function name and callback. 28 | Implementation for global mock. 29 | """ 30 | def target_methods() do 31 | [ 32 | {:request, &ExVCR.Recorder.request([&1, &2])}, 33 | {:request, &ExVCR.Recorder.request([&1, &2, &3])}, 34 | {:request!, &(ExVCR.Recorder.request([&1, &2]) |> handle_response_for_request!())}, 35 | {:request!, &(ExVCR.Recorder.request([&1, &2, &3]) |> handle_response_for_request!())} 36 | ] 37 | end 38 | 39 | @doc """ 40 | Returns list of the mock target methods with function name and callback. 41 | """ 42 | def target_methods(recorder) do 43 | [ 44 | {:request, &ExVCR.Recorder.request(recorder, [&1, &2])}, 45 | {:request, &ExVCR.Recorder.request(recorder, [&1, &2, &3])}, 46 | {:request!, &(ExVCR.Recorder.request(recorder, [&1, &2]) |> handle_response_for_request!())}, 47 | {:request!, &(ExVCR.Recorder.request(recorder, [&1, &2, &3]) |> handle_response_for_request!())} 48 | ] 49 | end 50 | 51 | @doc """ 52 | Generate key for searching response. 53 | """ 54 | def generate_keys_for_request(request) do 55 | req = Enum.fetch!(request, 0) 56 | url = Util.build_url(req.scheme, req.host, req.path, req.port, req.query) 57 | 58 | [url: url, method: String.downcase(req.method), request_body: req.body, headers: req.headers] 59 | end 60 | 61 | @doc """ 62 | Callback from ExVCR.Handler when response is retrieved from the HTTP server. 63 | """ 64 | def hook_response_from_server(response) do 65 | apply_filters(response) 66 | end 67 | 68 | @doc """ 69 | Callback from ExVCR.Handler to get the response content tuple from the ExVCR.Response record. 70 | """ 71 | def get_response_value_from_cache(response) do 72 | if response.type == "error" do 73 | {:error, response.body} 74 | else 75 | finch_response = %Finch.Response{ 76 | status: response.status_code, 77 | headers: response.headers, 78 | body: response.body 79 | } 80 | 81 | {:ok, finch_response} 82 | end 83 | end 84 | 85 | defp apply_filters({:ok, %Finch.Response{} = response}) do 86 | filtered_response = apply_filters(response) 87 | {:ok, filtered_response} 88 | end 89 | 90 | defp apply_filters(%Finch.Response{} = response) do 91 | replaced_body = to_string(response.body) |> ExVCR.Filter.filter_sensitive_data() 92 | filtered_headers = ExVCR.Filter.remove_blacklisted_headers(response.headers) 93 | 94 | response 95 | |> Map.put(:body, replaced_body) 96 | |> Map.put(:headers, filtered_headers) 97 | end 98 | 99 | defp apply_filters({:error, reason}), do: {:error, reason} 100 | 101 | defp handle_response_for_request!({:ok, resp}), do: resp 102 | defp handle_response_for_request!({:error, error}), do: raise(error) 103 | defp handle_response_for_request!(resp), do: resp 104 | 105 | @doc """ 106 | Default definitions for stub. 107 | """ 108 | def default_stub_params(:headers), do: %{"content-type" => "text/html"} 109 | def default_stub_params(:status_code), do: 200 110 | end 111 | else 112 | defmodule ExVCR.Adapter.Finch do 113 | def module_name, do: raise("Missing dependency: Finch") 114 | def target_methods, do: raise("Missing dependency: Finch") 115 | end 116 | end 117 | -------------------------------------------------------------------------------- /lib/exvcr/adapter/finch/converter.ex: -------------------------------------------------------------------------------- 1 | if Code.ensure_loaded?(Finch) do 2 | defmodule ExVCR.Adapter.Finch.Converter do 3 | @moduledoc """ 4 | Provides helpers to mock Finch methods. 5 | """ 6 | 7 | use ExVCR.Converter 8 | 9 | alias ExVCR.Util 10 | 11 | defp string_to_response(string) do 12 | response = Enum.map(string, fn {x, y} -> {String.to_atom(x), y} end) 13 | response = struct(ExVCR.Response, response) 14 | 15 | response = 16 | if response.type == "error" do 17 | body = string_to_error_reason(response.body) 18 | %{response | body: body} 19 | else 20 | response 21 | end 22 | 23 | response = 24 | if is_map(response.headers) do 25 | headers = 26 | response.headers 27 | |> Map.to_list() 28 | |> Enum.map(fn {k, v} -> {k, v} end) 29 | 30 | %{response | headers: headers} 31 | else 32 | response 33 | end 34 | 35 | response 36 | end 37 | 38 | defp string_to_error_reason(reason) do 39 | {reason_struct, _} = Code.eval_string(reason) 40 | reason_struct 41 | end 42 | 43 | defp request_to_string([request, finch_module]) do 44 | request_to_string([request, finch_module, []]) 45 | end 46 | 47 | defp request_to_string([request, _finch_module, opts]) do 48 | url = 49 | Util.build_url(request.scheme, request.host, request.path, request.port, request.query) 50 | 51 | %ExVCR.Request{ 52 | url: parse_url(url), 53 | headers: parse_headers(request.headers), 54 | method: String.downcase(request.method), 55 | body: parse_request_body(request.body), 56 | options: parse_options(sanitize_options(opts)) 57 | } 58 | end 59 | 60 | # If option value is tuple, make it as list, for encoding as json. 61 | defp sanitize_options(options) do 62 | Enum.map(options, fn {key, value} -> 63 | if is_tuple(value) do 64 | {key, Tuple.to_list(value)} 65 | else 66 | {key, value} 67 | end 68 | end) 69 | end 70 | 71 | defp response_to_string({:ok, %Finch.Response{} = response}), do: response_to_string(response) 72 | 73 | defp response_to_string(%Finch.Response{} = response) do 74 | %ExVCR.Response{ 75 | type: "ok", 76 | status_code: response.status, 77 | headers: parse_headers(response.headers), 78 | body: to_string(response.body) 79 | } 80 | end 81 | 82 | defp response_to_string({:error, reason}) do 83 | %ExVCR.Response{ 84 | type: "error", 85 | body: error_reason_to_string(reason) 86 | } 87 | end 88 | 89 | defp error_reason_to_string(reason), do: Macro.to_string(reason) 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /lib/exvcr/adapter/hackney.ex: -------------------------------------------------------------------------------- 1 | defmodule ExVCR.Adapter.Hackney do 2 | @moduledoc """ 3 | Provides adapter methods to mock :hackney methods. 4 | """ 5 | 6 | use ExVCR.Adapter 7 | alias ExVCR.Adapter.Hackney.Store 8 | alias ExVCR.Util 9 | 10 | defmacro __using__(_opts) do 11 | quote do 12 | Store.start() 13 | end 14 | end 15 | 16 | defdelegate convert_from_string(string), to: ExVCR.Adapter.Hackney.Converter 17 | defdelegate convert_to_string(request, response), to: ExVCR.Adapter.Hackney.Converter 18 | defdelegate parse_request_body(request_body), to: ExVCR.Adapter.Hackney.Converter 19 | 20 | @doc """ 21 | Returns the name of the mock target module. 22 | """ 23 | def module_name do 24 | :hackney 25 | end 26 | 27 | @doc """ 28 | Returns list of the mock target methods with function name and callback. 29 | Implementation for global mock. 30 | """ 31 | def target_methods() do 32 | [ 33 | {:request, &ExVCR.Recorder.request([&1, &2, &3, &4, &5])}, 34 | {:request, &ExVCR.Recorder.request([&1, &2, &3, &4])}, 35 | {:request, &ExVCR.Recorder.request([&1, &2, &3])}, 36 | {:request, &ExVCR.Recorder.request([&1, &2])}, 37 | {:request, &ExVCR.Recorder.request([&1])}, 38 | {:body, &handle_body_request([&1])}, 39 | {:body, &handle_body_request([&1, &2])} 40 | ] 41 | end 42 | 43 | @doc """ 44 | Returns list of the mock target methods with function name and callback. 45 | """ 46 | def target_methods(recorder) do 47 | [ 48 | {:request, &ExVCR.Recorder.request(recorder, [&1, &2, &3, &4, &5])}, 49 | {:request, &ExVCR.Recorder.request(recorder, [&1, &2, &3, &4])}, 50 | {:request, &ExVCR.Recorder.request(recorder, [&1, &2, &3])}, 51 | {:request, &ExVCR.Recorder.request(recorder, [&1, &2])}, 52 | {:request, &ExVCR.Recorder.request(recorder, [&1])}, 53 | {:body, &handle_body_request(recorder, [&1])}, 54 | {:body, &handle_body_request(recorder, [&1, &2])} 55 | ] 56 | end 57 | 58 | @doc """ 59 | Generate key for searching response. 60 | """ 61 | def generate_keys_for_request(request) do 62 | url = Enum.fetch!(request, 1) 63 | method = Enum.fetch!(request, 0) 64 | request_body = Enum.fetch(request, 3) |> parse_request_body() 65 | headers = Enum.at(request, 2, []) |> Util.stringify_keys() 66 | 67 | [url: url, method: method, request_body: request_body, headers: headers] 68 | end 69 | 70 | @doc """ 71 | Callback from ExVCR.Handler when response is retrieved from the HTTP server. 72 | """ 73 | def hook_response_from_server(response) do 74 | apply_filters(response) 75 | end 76 | 77 | defp apply_filters({:ok, status_code, headers, reference}) do 78 | filtered_headers = ExVCR.Filter.remove_blacklisted_headers(headers) 79 | {:ok, status_code, filtered_headers, reference} 80 | end 81 | 82 | defp apply_filters({:ok, status_code, headers}) do 83 | filtered_headers = ExVCR.Filter.remove_blacklisted_headers(headers) 84 | {:ok, status_code, filtered_headers} 85 | end 86 | 87 | defp apply_filters({:error, reason}) do 88 | {:error, reason} 89 | end 90 | 91 | @doc """ 92 | Callback from ExVCR.Handler when response is retrieved from the json file cache. 93 | """ 94 | def hook_response_from_cache(_request, nil), do: nil 95 | def hook_response_from_cache(_request, %ExVCR.Response{type: "error"} = response), do: response 96 | def hook_response_from_cache(_request, %ExVCR.Response{body: nil} = response), do: response 97 | 98 | def hook_response_from_cache([_, _, _, _, opts], %ExVCR.Response{body: body} = response) do 99 | if :with_body in opts || {:with_body, true} in opts do 100 | response 101 | else 102 | client = make_ref() 103 | client_key_atom = client |> inspect() |> String.to_atom() 104 | Store.set(client_key_atom, body) 105 | %{response | body: client} 106 | end 107 | end 108 | 109 | defp handle_body_request(args) do 110 | ExVCR.Actor.CurrentRecorder.get() 111 | |> handle_body_request(args) 112 | end 113 | 114 | defp handle_body_request(nil, args) do 115 | :meck.passthrough(args) 116 | end 117 | 118 | defp handle_body_request(recorder, [client]) do 119 | handle_body_request(recorder, [client, :infinity]) 120 | end 121 | 122 | defp handle_body_request(recorder, [client, max_length]) do 123 | client_key_atom = client |> inspect() |> String.to_atom() 124 | 125 | if body = Store.get(client_key_atom) do 126 | Store.delete(client_key_atom) 127 | {:ok, body} 128 | else 129 | case :meck.passthrough([client, max_length]) do 130 | {:ok, body} -> 131 | body = ExVCR.Filter.filter_sensitive_data(body) 132 | 133 | client_key_string = inspect(client) 134 | 135 | ExVCR.Recorder.update( 136 | recorder, 137 | fn %{request: _request, response: response} -> 138 | response.body == client_key_string 139 | end, 140 | fn %{request: request, response: response} -> 141 | %{request: request, response: %{response | body: body}} 142 | end 143 | ) 144 | 145 | {:ok, body} 146 | 147 | {ret, body} -> 148 | {ret, body} 149 | end 150 | end 151 | end 152 | 153 | @doc """ 154 | Returns the response from the ExVCR.Response record. 155 | """ 156 | def get_response_value_from_cache(response) do 157 | if response.type == "error" do 158 | {:error, response.body} 159 | else 160 | case response.body do 161 | nil -> {:ok, response.status_code, response.headers} 162 | _ -> {:ok, response.status_code, response.headers, response.body} 163 | end 164 | end 165 | end 166 | 167 | @doc """ 168 | Default definitions for stub. 169 | """ 170 | def default_stub_params(:headers), do: %{"Content-Type" => "text/html"} 171 | def default_stub_params(:status_code), do: 200 172 | end 173 | -------------------------------------------------------------------------------- /lib/exvcr/adapter/hackney/converter.ex: -------------------------------------------------------------------------------- 1 | defmodule ExVCR.Adapter.Hackney.Converter do 2 | @moduledoc """ 3 | Provides helpers to mock :hackney methods. 4 | """ 5 | 6 | use ExVCR.Converter 7 | 8 | defp string_to_response(string) do 9 | response = Enum.map(string, fn {x, y} -> {String.to_atom(x), y} end) 10 | response = struct(ExVCR.Response, response) 11 | 12 | response = 13 | if is_map(response.headers) do 14 | headers = response.headers |> Map.to_list() 15 | %{response | headers: headers} 16 | else 17 | response 18 | end 19 | 20 | response 21 | end 22 | 23 | defp request_to_string(request) do 24 | method = Enum.fetch!(request, 0) |> to_string() 25 | url = Enum.fetch!(request, 1) |> parse_url() 26 | headers = Enum.at(request, 2, []) |> parse_headers() 27 | body = Enum.at(request, 3, "") |> parse_request_body() 28 | options = Enum.at(request, 4, []) |> sanitize_options() |> parse_options() 29 | 30 | %ExVCR.Request{ 31 | url: url, 32 | headers: headers, 33 | method: method, 34 | body: body, 35 | options: options 36 | } 37 | end 38 | 39 | # Sanitize options so that they can be encoded as json. 40 | defp sanitize_options(options) do 41 | Enum.map(options, &do_sanitize/1) 42 | end 43 | 44 | defp do_sanitize({key, value}) when is_function(key) do 45 | {inspect(key), value} 46 | end 47 | 48 | defp do_sanitize({key, value}) when is_list(value) do 49 | {key, Enum.map(value, &do_sanitize/1)} 50 | end 51 | 52 | defp do_sanitize({key, value}) when is_tuple(value) do 53 | {key, Tuple.to_list(do_sanitize(value))} 54 | end 55 | 56 | defp do_sanitize({key, value}) when is_function(value) do 57 | {key, inspect(value)} 58 | end 59 | 60 | defp do_sanitize({key, value}) do 61 | {key, value} 62 | end 63 | 64 | defp do_sanitize(key) when is_atom(key) do 65 | {key, true} 66 | end 67 | 68 | defp do_sanitize(value) do 69 | value 70 | end 71 | 72 | defp response_to_string({:ok, status_code, headers, body_or_client}) do 73 | body = 74 | case body_or_client do 75 | string when is_binary(string) -> string 76 | # Client is already replaced by body through ExVCR.Adapter.Hackney adapter. 77 | ref when is_reference(ref) -> inspect(ref) 78 | end 79 | 80 | %ExVCR.Response{ 81 | type: "ok", 82 | status_code: status_code, 83 | headers: parse_headers(headers), 84 | body: body 85 | } 86 | end 87 | 88 | defp response_to_string({:ok, status_code, headers}) do 89 | %ExVCR.Response{ 90 | type: "ok", 91 | status_code: status_code, 92 | headers: parse_headers(headers) 93 | } 94 | end 95 | 96 | defp response_to_string({:error, reason}) do 97 | %ExVCR.Response{ 98 | type: "error", 99 | body: Atom.to_string(reason) 100 | } 101 | end 102 | 103 | def parse_request_body({:form, body}) do 104 | hackney_request_module().encode_form(body) 105 | |> elem(2) 106 | |> to_string() 107 | |> ExVCR.Filter.filter_sensitive_data() 108 | end 109 | 110 | def parse_request_body(body), do: super(body) 111 | 112 | defp hackney_request_module(), do: :hackney_request 113 | end 114 | -------------------------------------------------------------------------------- /lib/exvcr/adapter/hackney/store.ex: -------------------------------------------------------------------------------- 1 | defmodule ExVCR.Adapter.Hackney.Store do 2 | @moduledoc """ 3 | Provides a datastore for temporary saving client key (Reference) and body relationship. 4 | """ 5 | 6 | @doc """ 7 | Initialize the datastore. 8 | """ 9 | def start do 10 | if :ets.info(table()) == :undefined do 11 | :ets.new(table(), [:set, :public, :named_table]) 12 | end 13 | 14 | :ok 15 | end 16 | 17 | @doc """ 18 | Returns value (body) from the key (client key). 19 | """ 20 | def get(key) do 21 | start() 22 | :ets.lookup(table(), key)[key] 23 | end 24 | 25 | @doc """ 26 | Set value (body) with the key (client key). 27 | """ 28 | def set(key, value) do 29 | start() 30 | :ets.insert(table(), {key, value}) 31 | value 32 | end 33 | 34 | @doc """ 35 | Set key (client key). 36 | """ 37 | def delete(key) do 38 | start() 39 | :ets.delete(table(), key) 40 | end 41 | 42 | defp table do 43 | "exvcr_hackney#{inspect(self())}" |> String.to_atom() 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/exvcr/adapter/httpc.ex: -------------------------------------------------------------------------------- 1 | defmodule ExVCR.Adapter.Httpc do 2 | @moduledoc """ 3 | Provides adapter methods to mock :httpc methods. 4 | """ 5 | 6 | use ExVCR.Adapter 7 | alias ExVCR.Util 8 | 9 | defmacro __using__(_opts) do 10 | # do nothing 11 | end 12 | 13 | defdelegate convert_from_string(string), to: ExVCR.Adapter.Httpc.Converter 14 | defdelegate convert_to_string(request, response), to: ExVCR.Adapter.Httpc.Converter 15 | defdelegate parse_request_body(request_body), to: ExVCR.Adapter.Httpc.Converter 16 | 17 | @doc """ 18 | Returns the name of the mock target module. 19 | """ 20 | def module_name do 21 | :httpc 22 | end 23 | 24 | @doc """ 25 | Returns list of the mock target methods with function name and callback. 26 | Implementation for global mock. 27 | TODO: 28 | {:request, &ExVCR.Recorder.request(recorder, [&1,&2])} 29 | {:request, &ExVCR.Recorder.request(recorder, [&1,&2,&3,&4,&5])} 30 | """ 31 | def target_methods() do 32 | [{:request, &ExVCR.Recorder.request([&1])}, {:request, &ExVCR.Recorder.request([&1, &2, &3, &4])}] 33 | end 34 | 35 | @doc """ 36 | Returns list of the mock target methods with function name and callback. 37 | TODO: 38 | {:request, &ExVCR.Recorder.request(recorder, [&1,&2])} 39 | {:request, &ExVCR.Recorder.request(recorder, [&1,&2,&3,&4,&5])} 40 | """ 41 | def target_methods(recorder) do 42 | [ 43 | {:request, &ExVCR.Recorder.request(recorder, [&1])}, 44 | {:request, &ExVCR.Recorder.request(recorder, [&1, &2, &3, &4])} 45 | ] 46 | end 47 | 48 | @doc """ 49 | Generate key for searching response. 50 | """ 51 | def generate_keys_for_request(request) do 52 | case request do 53 | [method, {url, headers} | _] -> 54 | [url: url, method: method, request_body: nil, headers: Util.stringify_keys(headers)] 55 | 56 | [method, {url, headers, _, body} | _] -> 57 | [url: url, method: method, request_body: body, headers: Util.stringify_keys(headers)] 58 | 59 | [url | _] -> 60 | [url: url, method: :get, request_body: nil, headers: []] 61 | end 62 | end 63 | 64 | @doc """ 65 | Callback from ExVCR.Handler when response is retrieved from the HTTP server. 66 | """ 67 | def hook_response_from_server(response) do 68 | apply_filters(response) 69 | end 70 | 71 | defp apply_filters({:ok, {status_code, headers, body}}) do 72 | replaced_body = to_string(body) |> ExVCR.Filter.filter_sensitive_data() 73 | filtered_headers = ExVCR.Filter.remove_blacklisted_headers(headers) 74 | {:ok, {status_code, filtered_headers, replaced_body}} 75 | end 76 | 77 | defp apply_filters({:error, reason}) do 78 | {:error, reason} 79 | end 80 | 81 | @doc """ 82 | Returns the response from the ExVCR.Response record. 83 | """ 84 | def get_response_value_from_cache(response) do 85 | if response.type == "error" do 86 | {:error, response.body} 87 | else 88 | {:ok, {response.status_code, response.headers, response.body}} 89 | end 90 | end 91 | 92 | @doc """ 93 | Default definitions for stub. 94 | """ 95 | def default_stub_params(:headers), do: %{"content-type" => "text/html"} 96 | def default_stub_params(:status_code), do: ["HTTP/1.1", 200, "OK"] 97 | end 98 | -------------------------------------------------------------------------------- /lib/exvcr/adapter/httpc/converter.ex: -------------------------------------------------------------------------------- 1 | defmodule ExVCR.Adapter.Httpc.Converter do 2 | @moduledoc """ 3 | Provides helpers to mock :httpc methods. 4 | """ 5 | 6 | use ExVCR.Converter 7 | 8 | defp string_to_response(string) do 9 | response = Enum.map(string, fn {x, y} -> {String.to_atom(x), y} end) 10 | response = struct(ExVCR.Response, response) 11 | 12 | response = 13 | if response.status_code do 14 | status_code = 15 | response.status_code 16 | |> Enum.map(&convert_string_to_charlist/1) 17 | |> List.to_tuple() 18 | 19 | %{response | status_code: status_code} 20 | else 21 | response 22 | end 23 | 24 | response = 25 | if response.type == "error" do 26 | %{response | body: {String.to_atom(response.body), []}} 27 | else 28 | response 29 | end 30 | 31 | response = 32 | if is_map(response.headers) do 33 | headers = 34 | response.headers 35 | |> Map.to_list() 36 | |> Enum.map(fn {k, v} -> {to_charlist(k), to_charlist(v)} end) 37 | 38 | %{response | headers: headers} 39 | else 40 | response 41 | end 42 | 43 | response 44 | end 45 | 46 | defp convert_string_to_charlist(elem) do 47 | if is_binary(elem) do 48 | to_charlist(elem) 49 | else 50 | elem 51 | end 52 | end 53 | 54 | defp request_to_string([url]) do 55 | request_to_string([:get, {url, [], [], []}, [], []]) 56 | end 57 | 58 | defp request_to_string([method, {url, headers}, http_options, options]) do 59 | request_to_string([method, {url, headers, [], []}, http_options, options]) 60 | end 61 | 62 | # TODO: need to handle content_type 63 | defp request_to_string([method, {url, headers, _content_type, body}, http_options, options]) do 64 | %ExVCR.Request{ 65 | url: parse_url(url), 66 | headers: parse_headers(headers), 67 | method: to_string(method), 68 | body: parse_request_body(body), 69 | options: [httpc_options: parse_keyword_list(options), http_options: parse_keyword_list(http_options)] 70 | } 71 | end 72 | 73 | defp response_to_string({:ok, {{http_version, status_code, reason_phrase}, headers, body}}) do 74 | %ExVCR.Response{ 75 | type: "ok", 76 | status_code: [to_string(http_version), status_code, to_string(reason_phrase)], 77 | headers: parse_headers(headers), 78 | body: to_string(body) 79 | } 80 | end 81 | 82 | defp response_to_string({:error, {reason, _detail}}) do 83 | %ExVCR.Response{ 84 | type: "error", 85 | body: Atom.to_string(reason) 86 | } 87 | end 88 | end 89 | -------------------------------------------------------------------------------- /lib/exvcr/adapter/ibrowse.ex: -------------------------------------------------------------------------------- 1 | defmodule ExVCR.Adapter.IBrowse do 2 | @moduledoc """ 3 | Provides adapter methods to mock :ibrowse methods. 4 | """ 5 | 6 | use ExVCR.Adapter 7 | alias ExVCR.Util 8 | 9 | defmacro __using__(_opts) do 10 | # do nothing 11 | end 12 | 13 | defdelegate convert_from_string(string), to: ExVCR.Adapter.IBrowse.Converter 14 | defdelegate convert_to_string(request, response), to: ExVCR.Adapter.IBrowse.Converter 15 | defdelegate parse_request_body(request_body), to: ExVCR.Adapter.IBrowse.Converter 16 | 17 | @doc """ 18 | Returns the name of the mock target module. 19 | """ 20 | def module_name do 21 | :ibrowse 22 | end 23 | 24 | @doc """ 25 | Returns list of the mock target methods with function name and callback. 26 | Implementation for global mock. 27 | """ 28 | def target_methods() do 29 | [ 30 | {:send_req, &ExVCR.Recorder.request([&1, &2, &3])}, 31 | {:send_req, &ExVCR.Recorder.request([&1, &2, &3, &4])}, 32 | {:send_req, &ExVCR.Recorder.request([&1, &2, &3, &4, &5])}, 33 | {:send_req, &ExVCR.Recorder.request([&1, &2, &3, &4, &5, &6])} 34 | ] 35 | end 36 | 37 | @doc """ 38 | Returns list of the mock target methods with function name and callback. 39 | """ 40 | def target_methods(recorder) do 41 | [ 42 | {:send_req, &ExVCR.Recorder.request(recorder, [&1, &2, &3])}, 43 | {:send_req, &ExVCR.Recorder.request(recorder, [&1, &2, &3, &4])}, 44 | {:send_req, &ExVCR.Recorder.request(recorder, [&1, &2, &3, &4, &5])}, 45 | {:send_req, &ExVCR.Recorder.request(recorder, [&1, &2, &3, &4, &5, &6])} 46 | ] 47 | end 48 | 49 | @doc """ 50 | Generate key for searching response. 51 | """ 52 | def generate_keys_for_request(request) do 53 | url = Enum.fetch!(request, 0) 54 | method = Enum.fetch!(request, 2) 55 | request_body = Enum.fetch(request, 3) |> parse_request_body() 56 | headers = Enum.fetch!(request, 1) |> Util.stringify_keys() 57 | 58 | [url: url, method: method, request_body: request_body, headers: headers] 59 | end 60 | 61 | @doc """ 62 | Callback from ExVCR.Handler when response is retrieved from the HTTP server. 63 | """ 64 | def hook_response_from_server(response) do 65 | apply_filters(response) 66 | end 67 | 68 | @doc """ 69 | Callback from ExVCR.Handler to get the response content tuple from the ExVCR.Response record. 70 | """ 71 | def get_response_value_from_cache(response) do 72 | if response.type == "error" do 73 | {:error, response.body} 74 | else 75 | status_code = 76 | case response.status_code do 77 | integer when is_integer(integer) -> 78 | Integer.to_charlist(integer) 79 | 80 | char_list when is_list(char_list) -> 81 | char_list 82 | end 83 | 84 | {:ok, status_code, response.headers, response.body} 85 | end 86 | end 87 | 88 | defp apply_filters({:ok, status_code, headers, body}) do 89 | replaced_body = to_string(body) |> ExVCR.Filter.filter_sensitive_data() 90 | filtered_headers = ExVCR.Filter.remove_blacklisted_headers(headers) 91 | {:ok, status_code, filtered_headers, replaced_body} 92 | end 93 | 94 | defp apply_filters({:error, reason}) do 95 | {:error, reason} 96 | end 97 | 98 | @doc """ 99 | Default definitions for stub. 100 | """ 101 | def default_stub_params(:headers), do: %{"Content-Type" => "text/html"} 102 | def default_stub_params(:status_code), do: 200 103 | end 104 | -------------------------------------------------------------------------------- /lib/exvcr/adapter/ibrowse/converter.ex: -------------------------------------------------------------------------------- 1 | defmodule ExVCR.Adapter.IBrowse.Converter do 2 | @moduledoc """ 3 | Provides helpers to mock :ibrowse methods. 4 | """ 5 | 6 | use ExVCR.Converter 7 | 8 | defp string_to_response(string) do 9 | response = Enum.map(string, fn {x, y} -> {String.to_atom(x), y} end) 10 | response = struct(ExVCR.Response, response) 11 | 12 | response = 13 | if response.status_code do 14 | %{response | status_code: Integer.to_charlist(response.status_code)} 15 | else 16 | response 17 | end 18 | 19 | response = 20 | if response.type == "error" do 21 | body = string_to_error_reason(response.body) 22 | %{response | body: body} 23 | else 24 | response 25 | end 26 | 27 | response = 28 | if is_map(response.headers) do 29 | headers = 30 | response.headers 31 | |> Map.to_list() 32 | |> Enum.map(fn {k, v} -> {to_charlist(k), to_charlist(v)} end) 33 | 34 | %{response | headers: headers} 35 | else 36 | response 37 | end 38 | 39 | response 40 | end 41 | 42 | defp string_to_error_reason([reason, details]), do: {String.to_atom(reason), binary_to_tuple(details)} 43 | defp string_to_error_reason([reason]), do: String.to_atom(reason) 44 | 45 | defp request_to_string([url, headers, method]), do: request_to_string([url, headers, method, [], []]) 46 | defp request_to_string([url, headers, method, body]), do: request_to_string([url, headers, method, body, []]) 47 | 48 | defp request_to_string([url, headers, method, body, options]), 49 | do: request_to_string([url, headers, method, body, options, 5000]) 50 | 51 | defp request_to_string([url, headers, method, body, options, _timeout]) do 52 | %ExVCR.Request{ 53 | url: parse_url(url), 54 | headers: parse_headers(headers), 55 | method: Atom.to_string(method), 56 | body: parse_request_body(body), 57 | options: parse_options(sanitize_options(options)) 58 | } 59 | end 60 | 61 | # If option value is tuple, make it as list, for encoding as json. 62 | defp sanitize_options(options) do 63 | Enum.map(options, fn {key, value} -> 64 | if is_tuple(value) do 65 | {key, Tuple.to_list(value)} 66 | else 67 | {key, value} 68 | end 69 | end) 70 | end 71 | 72 | defp response_to_string({:ok, status_code, headers, body}) do 73 | %ExVCR.Response{ 74 | type: "ok", 75 | status_code: List.to_integer(status_code), 76 | headers: parse_headers(headers), 77 | body: to_string(body) 78 | } 79 | end 80 | 81 | defp response_to_string({:error, reason}) do 82 | %ExVCR.Response{ 83 | type: "error", 84 | body: error_reason_to_string(reason) 85 | } 86 | end 87 | 88 | defp error_reason_to_string({reason, details}), do: [Atom.to_string(reason), tuple_to_binary(details)] 89 | defp error_reason_to_string(reason), do: [Atom.to_string(reason)] 90 | 91 | defp tuple_to_binary(tuple) do 92 | Enum.map(Tuple.to_list(tuple), fn x -> 93 | if is_atom(x), do: Atom.to_string(x), else: x 94 | end) 95 | end 96 | 97 | defp binary_to_tuple(list) do 98 | Enum.map(list, fn x -> 99 | if is_binary(x), do: String.to_atom(x), else: x 100 | end) 101 | |> List.to_tuple() 102 | end 103 | end 104 | -------------------------------------------------------------------------------- /lib/exvcr/application.ex: -------------------------------------------------------------------------------- 1 | defmodule ExVCR.Application do 2 | use Application 3 | 4 | def start(_type, _args) do 5 | children = 6 | if global_mock_enabled?() do 7 | globally_mock_adapters() 8 | [ExVCR.Actor.CurrentRecorder] 9 | else 10 | [] 11 | end 12 | 13 | Supervisor.start_link(children, strategy: :one_for_one, name: ExVCR.Supervisor) 14 | end 15 | 16 | defp globally_mock_adapters do 17 | for app <- [:hackney, :ibrowse, :httpc, Finch], true == Code.ensure_loaded?(app) do 18 | app 19 | |> target_methods() 20 | |> Enum.each(fn {function, callback} -> 21 | :meck.expect(app, function, callback) 22 | end) 23 | end 24 | end 25 | 26 | defp target_methods(:hackney), do: ExVCR.Adapter.Hackney.target_methods() 27 | defp target_methods(:ibrowse), do: ExVCR.Adapter.IBrowse.target_methods() 28 | defp target_methods(:httpc), do: ExVCR.Adapter.Httpc.target_methods() 29 | defp target_methods(Finch), do: ExVCR.Adapter.Finch.target_methods() 30 | 31 | def global_mock_enabled? do 32 | Application.get_env(:exvcr, :global_mock, false) 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/exvcr/checker.ex: -------------------------------------------------------------------------------- 1 | defmodule ExVCR.Checker do 2 | @moduledoc """ 3 | Provides data store for checking which cassette files are used. 4 | It's for [mix vcr.check] task. 5 | """ 6 | 7 | use GenServer 8 | 9 | def start(arg) do 10 | GenServer.start(__MODULE__, arg, name: :singleton) 11 | end 12 | 13 | def get do 14 | GenServer.call(:singleton, :get) 15 | end 16 | 17 | def set(x) do 18 | GenServer.cast(:singleton, {:set, x}) 19 | end 20 | 21 | def append(x) do 22 | GenServer.cast(:singleton, {:append, x}) 23 | end 24 | 25 | @doc """ 26 | Increment the counter for cache cassettes hit. 27 | """ 28 | def add_cache_count(recorder), do: add_count(recorder, :cache) 29 | 30 | @doc """ 31 | Increment the counter for server request hit. 32 | """ 33 | def add_server_count(recorder), do: add_count(recorder, :server) 34 | 35 | defp add_count(recorder, type) do 36 | if ExVCR.Checker.get() != [] do 37 | ExVCR.Checker.append({type, ExVCR.Recorder.get_file_path(recorder)}) 38 | end 39 | end 40 | 41 | # Callbacks 42 | 43 | @impl true 44 | def init(arg) do 45 | {:ok, arg} 46 | end 47 | 48 | @impl true 49 | def handle_call(:get, _from, state) do 50 | {:reply, state, state} 51 | end 52 | 53 | @impl true 54 | def handle_cast({:set, x}, _state) do 55 | {:noreply, x} 56 | end 57 | 58 | @impl true 59 | def handle_cast({:append, x}, state) do 60 | {:noreply, %{state | files: [x | state.files]}} 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /lib/exvcr/config.ex: -------------------------------------------------------------------------------- 1 | defmodule ExVCR.Config do 2 | @moduledoc """ 3 | Assign configuration parameters. 4 | """ 5 | 6 | alias ExVCR.Setting 7 | 8 | @doc """ 9 | Initializes library dir to store cassette json files. 10 | - vcr_dir: directory for storing recorded json file. 11 | - custom_dir: directory for placing custom json file. 12 | """ 13 | def cassette_library_dir(vcr_dir, custom_dir \\ nil) do 14 | Setting.set(:cassette_library_dir, vcr_dir) 15 | Setting.set(:custom_library_dir, custom_dir) 16 | :ok 17 | end 18 | 19 | @doc """ 20 | Replace the specified pattern with placeholder. 21 | It can be used to remove sensitive data from the cassette file. 22 | 23 | ## Examples 24 | 25 | test "replace sensitive data" do 26 | ExVCR.Config.filter_sensitive_data(".+", "PLACEHOLDER") 27 | 28 | use_cassette "sensitive_data" do 29 | assert HTTPoison.get!("http://something.example.com", []).body =~ ~r/PLACEHOLDER/ 30 | end 31 | 32 | # Now clear the previous filter 33 | ExVCR.Config.filter_sensitive_data(nil) 34 | end 35 | """ 36 | def filter_sensitive_data(pattern, placeholder) do 37 | Setting.append(:filter_sensitive_data, {pattern, placeholder}) 38 | end 39 | 40 | def filter_sensitive_data(nil) do 41 | Setting.set(:filter_sensitive_data, []) 42 | end 43 | 44 | @doc """ 45 | This function can be used to filter headers from saved requests. 46 | 47 | ## Examples 48 | 49 | test "replace sensitive data in request header" do 50 | ExVCR.Config.filter_request_headers("X-My-Secret-Token") 51 | 52 | use_cassette "sensitive_data_in_request_header" do 53 | body = HTTPoison.get!("http://localhost:34000/server?", ["X-My-Secret-Token": "my-secret-token"]).body 54 | assert body == "test_response" 55 | end 56 | 57 | # The recorded cassette should contain replaced data. 58 | cassette = File.read!("sensitive_data_in_request_header.json") 59 | assert cassette =~ "\"X-My-Secret-Token\": \"***\"" 60 | refute cassette =~ "\"X-My-Secret-Token\": \"my-secret-token\"" 61 | 62 | # Now reset the filter 63 | ExVCR.Config.filter_request_headers(nil) 64 | end 65 | """ 66 | def filter_request_headers(nil) do 67 | Setting.set(:filter_request_headers, []) 68 | end 69 | 70 | def filter_request_headers(header) do 71 | Setting.append(:filter_request_headers, header) 72 | end 73 | 74 | @doc """ 75 | This function can be used to filter options. 76 | 77 | ## Examples 78 | 79 | test "replace sensitive data in request options" do 80 | ExVCR.Config.filter_request_options("basic_auth") 81 | use_cassette "sensitive_data_in_request_options" do 82 | body = HTTPoison.get!(@url, [], [hackney: [basic_auth: {"username", "password"}]]).body 83 | assert body == "test_response" 84 | end 85 | 86 | # The recorded cassette should contain replaced data. 87 | cassette = File.read!("sensitive_data_in_request_options.json") 88 | assert cassette =~ "\"basic_auth\": \"***\"" 89 | refute cassette =~ "\"basic_auth\": {\"username\", \"password\"}" 90 | 91 | # Now reset the filter 92 | ExVCR.Config.filter_request_options(nil) 93 | end 94 | """ 95 | def filter_request_options(nil) do 96 | Setting.set(:filter_request_options, []) 97 | end 98 | 99 | def filter_request_options(header) do 100 | Setting.append(:filter_request_options, header) 101 | end 102 | 103 | @doc """ 104 | Set the flag whether to filter-out url params when recording to cassettes. 105 | (ex. if flag is true, "param=val" is removed from "http://example.com?param=val"). 106 | """ 107 | def filter_url_params(flag) do 108 | Setting.set(:filter_url_params, flag) 109 | end 110 | 111 | @doc """ 112 | Sets a list of headers to remove from the response 113 | """ 114 | def response_headers_blacklist(headers_blacklist) do 115 | blacklist = Enum.map(headers_blacklist, fn x -> String.downcase(x) end) 116 | Setting.set(:response_headers_blacklist, blacklist) 117 | end 118 | 119 | @doc """ 120 | Skip recording cassettes for localhost requests when set 121 | """ 122 | def ignore_localhost(value) do 123 | Setting.set(:ignore_localhost, value) 124 | end 125 | 126 | @doc """ 127 | Skip recording cassettes for urls requests when set 128 | """ 129 | def ignore_urls(value) do 130 | Setting.set(:ignore_urls, value) 131 | end 132 | 133 | @doc """ 134 | Throw error if there is no matching cassette for an HTTP request 135 | """ 136 | def strict_mode(value) do 137 | Setting.set(:strict_mode, value) 138 | end 139 | end 140 | -------------------------------------------------------------------------------- /lib/exvcr/config_loader.ex: -------------------------------------------------------------------------------- 1 | defmodule ExVCR.ConfigLoader do 2 | @moduledoc """ 3 | Load configuration parameters from config.exs. 4 | """ 5 | 6 | @default_vcr_path "fixture/vcr_cassettes" 7 | @default_custom_path "fixture/custom_cassettes" 8 | 9 | alias ExVCR.Config 10 | 11 | @doc """ 12 | Load default config values. 13 | """ 14 | def load_defaults do 15 | env = Application.get_all_env(:exvcr) 16 | 17 | if env[:vcr_cassette_library_dir] != nil do 18 | Config.cassette_library_dir( 19 | env[:vcr_cassette_library_dir], 20 | env[:custom_cassette_library_dir] 21 | ) 22 | else 23 | Config.cassette_library_dir( 24 | @default_vcr_path, 25 | @default_custom_path 26 | ) 27 | end 28 | 29 | # reset to empty list 30 | Config.filter_sensitive_data(nil) 31 | 32 | if env[:filter_sensitive_data] != nil do 33 | Enum.each(env[:filter_sensitive_data], fn data -> 34 | Config.filter_sensitive_data(data[:pattern], data[:placeholder]) 35 | end) 36 | end 37 | 38 | # reset to empty list 39 | Config.filter_request_headers(nil) 40 | 41 | if env[:filter_request_headers] != nil do 42 | Enum.each(env[:filter_request_headers], fn header -> 43 | Config.filter_request_headers(header) 44 | end) 45 | end 46 | 47 | # reset to empty list 48 | Config.filter_request_options(nil) 49 | 50 | if env[:filter_request_options] != nil do 51 | Enum.each(env[:filter_request_options], fn option -> 52 | Config.filter_request_options(option) 53 | end) 54 | end 55 | 56 | if env[:filter_url_params] != nil do 57 | Config.filter_url_params(env[:filter_url_params]) 58 | end 59 | 60 | if env[:response_headers_blacklist] != nil do 61 | Config.response_headers_blacklist(env[:response_headers_blacklist]) 62 | else 63 | Config.response_headers_blacklist([]) 64 | end 65 | 66 | if env[:ignore_localhost] != nil do 67 | Config.ignore_localhost(env[:ignore_localhost]) 68 | end 69 | 70 | if env[:ignore_urls] != nil do 71 | Config.ignore_urls(env[:ignore_urls]) 72 | end 73 | 74 | if env[:strict_mode] != nil do 75 | Config.strict_mode(env[:strict_mode]) 76 | end 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /lib/exvcr/converter.ex: -------------------------------------------------------------------------------- 1 | defmodule ExVCR.Converter do 2 | @moduledoc """ 3 | Provides helpers for adapter converters. 4 | """ 5 | 6 | defmacro __using__(_) do 7 | quote do 8 | @doc """ 9 | Parse string format into original request / response tuples. 10 | """ 11 | def convert_from_string(%{"request" => request, "response" => response}) do 12 | %{request: string_to_request(request), response: string_to_response(response)} 13 | end 14 | 15 | defoverridable convert_from_string: 1 16 | 17 | @doc """ 18 | Parse request and response tuples into string format. 19 | """ 20 | def convert_to_string(request, response) do 21 | %{request: request_to_string(request), response: response_to_string(response)} 22 | end 23 | 24 | defoverridable convert_to_string: 2 25 | 26 | def string_to_request(string) do 27 | request = Enum.map(string, fn {x, y} -> {String.to_atom(x), y} end) |> Enum.into(%{}) 28 | struct(ExVCR.Request, request) 29 | end 30 | 31 | defoverridable string_to_request: 1 32 | 33 | def string_to_response(string), do: raise(ExVCR.ImplementationMissingError) 34 | defoverridable string_to_response: 1 35 | 36 | def request_to_string(request), do: raise(ExVCR.ImplementationMissingError) 37 | defoverridable request_to_string: 1 38 | 39 | def response_to_string(response), do: raise(ExVCR.ImplementationMissingError) 40 | defoverridable response_to_string: 1 41 | 42 | def parse_headers(headers) do 43 | do_parse_headers(headers, []) 44 | end 45 | 46 | defoverridable parse_headers: 1 47 | 48 | def do_parse_headers([], acc) do 49 | Enum.reverse(acc) |> Enum.uniq_by(fn {key, value} -> key end) 50 | end 51 | 52 | def do_parse_headers([{key, value} | tail], acc) do 53 | replaced_value = to_string(value) |> ExVCR.Filter.filter_sensitive_data() 54 | replaced_value = ExVCR.Filter.filter_request_header(to_string(key), to_string(replaced_value)) 55 | do_parse_headers(tail, [{to_string(key), replaced_value} | acc]) 56 | end 57 | 58 | defoverridable do_parse_headers: 2 59 | 60 | def parse_options(options) do 61 | do_parse_options(options, []) 62 | end 63 | 64 | defoverridable parse_options: 1 65 | 66 | def do_parse_options([], acc) do 67 | Enum.reverse(acc) |> Enum.uniq_by(fn {key, value} -> key end) 68 | end 69 | 70 | def do_parse_options([{key, value} | tail], acc) when is_function(value) do 71 | do_parse_options(tail, acc) 72 | end 73 | 74 | def do_parse_options([{key, value} | tail], acc) do 75 | replaced_value = atom_to_string(value) |> ExVCR.Filter.filter_sensitive_data() 76 | replaced_value = ExVCR.Filter.filter_request_option(to_string(key), atom_to_string(replaced_value)) 77 | do_parse_options(tail, [{to_string(key), replaced_value} | acc]) 78 | end 79 | 80 | defoverridable do_parse_options: 2 81 | 82 | def parse_url(url) do 83 | to_string(url) |> ExVCR.Filter.filter_url_params() 84 | end 85 | 86 | defoverridable parse_url: 1 87 | 88 | def parse_keyword_list(params) do 89 | Enum.map(params, fn {k, v} -> {k, to_string(v)} end) 90 | end 91 | 92 | defoverridable parse_keyword_list: 1 93 | 94 | def parse_request_body(:error), do: "" 95 | 96 | def parse_request_body({:ok, body}) do 97 | parse_request_body(body) 98 | end 99 | 100 | def parse_request_body(body) do 101 | body_string = 102 | try do 103 | to_string(body) 104 | rescue 105 | _e in Protocol.UndefinedError -> inspect(body) 106 | end 107 | 108 | ExVCR.Filter.filter_sensitive_data(body_string) 109 | end 110 | 111 | defoverridable parse_request_body: 1 112 | 113 | defp atom_to_string(atom) do 114 | if is_atom(atom) do 115 | to_string(atom) 116 | else 117 | atom 118 | end 119 | end 120 | end 121 | end 122 | end 123 | -------------------------------------------------------------------------------- /lib/exvcr/exceptions.ex: -------------------------------------------------------------------------------- 1 | defmodule ExVCR.InvalidRequestError, do: defexception([:message]) 2 | defmodule ExVCR.FileNotFoundError, do: defexception([:message]) 3 | defmodule ExVCR.PathNotFoundError, do: defexception([:message]) 4 | defmodule ExVCR.ImplementationMissingError, do: defexception([:message]) 5 | defmodule ExVCR.RequestNotMatchError, do: defexception([:message]) 6 | -------------------------------------------------------------------------------- /lib/exvcr/filter.ex: -------------------------------------------------------------------------------- 1 | defmodule ExVCR.Filter do 2 | @moduledoc """ 3 | Provide filters for request/responses. 4 | """ 5 | 6 | @doc """ 7 | Filter out sensitive data from the response. 8 | """ 9 | def filter_sensitive_data(body) when is_binary(body) do 10 | if String.valid?(body) do 11 | replace(body, ExVCR.Setting.get(:filter_sensitive_data)) 12 | else 13 | body 14 | end 15 | end 16 | 17 | def filter_sensitive_data(body), do: body 18 | 19 | @doc """ 20 | Filter out sensitive data from the request header. 21 | """ 22 | def filter_request_header(header, value) do 23 | if Enum.member?(ExVCR.Setting.get(:filter_request_headers), header), do: "***", else: value 24 | end 25 | 26 | @doc """ 27 | Filter out sensitive data from the request options. 28 | """ 29 | def filter_request_option(option, value) do 30 | if Enum.member?(ExVCR.Setting.get(:filter_request_options), option), do: "***", else: value 31 | end 32 | 33 | defp replace(body, []), do: body 34 | 35 | defp replace(body, [{pattern, placeholder} | tail]) do 36 | replace(String.replace(body, ~r/#{pattern}/, placeholder), tail) 37 | end 38 | 39 | @doc """ 40 | Filter out query params from the url. 41 | """ 42 | def filter_url_params(url) do 43 | if ExVCR.Setting.get(:filter_url_params) do 44 | strip_query_params(url) 45 | else 46 | url 47 | end 48 | |> filter_sensitive_data() 49 | end 50 | 51 | @doc """ 52 | Remove query params from the specified url. 53 | """ 54 | def strip_query_params(url) do 55 | url |> String.replace(~r/\?.+$/, "") 56 | end 57 | 58 | @doc """ 59 | Removes the headers listed in the response headers blacklist 60 | from the headers 61 | """ 62 | def remove_blacklisted_headers([]), do: [] 63 | 64 | def remove_blacklisted_headers(headers) do 65 | Enum.filter(headers, fn {key, _value} -> 66 | is_header_allowed?(key) 67 | end) 68 | end 69 | 70 | defp is_header_allowed?(header_name) do 71 | Enum.find(ExVCR.Setting.get(:response_headers_blacklist), fn x -> 72 | String.downcase(to_string(header_name)) == x 73 | end) == nil 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /lib/exvcr/iex.ex: -------------------------------------------------------------------------------- 1 | defmodule ExVCR.IEx do 2 | @moduledoc """ 3 | Provides helper functions for IEx. 4 | """ 5 | 6 | alias ExVCR.Recorder 7 | 8 | @doc """ 9 | Provides helper for monitoring http request/response in cassette json format. 10 | """ 11 | defmacro print(options \\ [], test) do 12 | adapter = options[:adapter] || ExVCR.Adapter.IBrowse 13 | method_name = :"ExVCR.IEx.Sample#{ExVCR.Util.uniq_id()}" 14 | 15 | quote do 16 | defmodule unquote(method_name) do 17 | use ExVCR.Mock, adapter: unquote(adapter) 18 | 19 | def run do 20 | recorder = Recorder.start(unquote(options) ++ [fixture: "", adapter: unquote(adapter)]) 21 | 22 | try do 23 | ExVCR.Mock.mock_methods(recorder, unquote(adapter)) 24 | unquote(test) 25 | after 26 | ExVCR.MockLock.release_lock() 27 | 28 | Recorder.get(recorder) 29 | |> JSX.encode!() 30 | |> JSX.prettify!() 31 | |> IO.puts() 32 | end 33 | 34 | :ok 35 | end 36 | end 37 | 38 | unquote(method_name).run() 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/exvcr/json.ex: -------------------------------------------------------------------------------- 1 | defmodule ExVCR.JSON do 2 | @moduledoc """ 3 | Provides a feature to store/load cassettes in json format. 4 | """ 5 | 6 | @doc """ 7 | Save responses into the json file. 8 | """ 9 | def save(file_name, recordings) do 10 | json = 11 | recordings 12 | |> Enum.map(&encode_binary_data/1) 13 | |> Enum.reverse() 14 | |> JSX.encode!() 15 | |> JSX.prettify!() 16 | 17 | if !File.exists?(path = Path.dirname(file_name)), do: File.mkdir_p!(path) 18 | File.write!(file_name, json) 19 | end 20 | 21 | defp encode_binary_data(%{request: _, response: %ExVCR.Response{body: nil}} = recording), do: recording 22 | 23 | defp encode_binary_data(%{response: response} = recording) do 24 | case String.valid?(response.body) do 25 | true -> 26 | recording 27 | 28 | false -> 29 | body = 30 | response.body 31 | |> :erlang.term_to_binary() 32 | |> Base.encode64() 33 | 34 | %{recording | response: %{response | body: body, binary: true}} 35 | end 36 | end 37 | 38 | @doc """ 39 | Loads the JSON files based on the fixture name and options. 40 | For options, this method just refers to the :custom attribute is set or not. 41 | """ 42 | def load(file_name, custom_mode, adapter) do 43 | case {File.exists?(file_name), custom_mode} do 44 | {true, _} -> read_json_file(file_name) |> Enum.map(&adapter.convert_from_string/1) 45 | {false, true} -> raise ExVCR.FileNotFoundError, message: "cassette file \"#{file_name}\" not found" 46 | {false, _} -> [] 47 | end 48 | end 49 | 50 | @doc """ 51 | Reads and parse the json file located at the specified file_name. 52 | """ 53 | def read_json_file(file_name) do 54 | file_name 55 | |> File.read!() 56 | |> JSX.decode!() 57 | |> Enum.map(&load_binary_data/1) 58 | end 59 | 60 | defp load_binary_data(%{"response" => %{"body" => body, "binary" => true} = response} = recording) do 61 | body = 62 | body 63 | |> Base.decode64!() 64 | |> :erlang.binary_to_term() 65 | 66 | %{recording | "response" => %{response | "body" => body}} 67 | end 68 | 69 | defp load_binary_data(recording), do: recording 70 | end 71 | -------------------------------------------------------------------------------- /lib/exvcr/mock.ex: -------------------------------------------------------------------------------- 1 | defmodule ExVCR.Mock do 2 | @moduledoc """ 3 | Provides macro to record HTTP request/response. 4 | """ 5 | 6 | alias ExVCR.Recorder 7 | 8 | defmacro __using__(opts) do 9 | adapter = opts[:adapter] || ExVCR.Adapter.IBrowse 10 | options = opts[:options] 11 | 12 | quote do 13 | import ExVCR.Mock 14 | :application.start(unquote(adapter).module_name()) 15 | use unquote(adapter) 16 | 17 | def adapter_method() do 18 | unquote(adapter) 19 | end 20 | 21 | def options_method() do 22 | unquote(options) 23 | end 24 | end 25 | end 26 | 27 | @doc """ 28 | Provides macro to trigger recording/replaying http interactions. 29 | 30 | ## Options 31 | 32 | - `:match_requests_on` A list of request properties to match on when 33 | finding a matching response. Valid values include `:query`, `:headers`, 34 | and `:request_body` 35 | 36 | """ 37 | defmacro use_cassette(:stub, options, test) do 38 | quote do 39 | stub_fixture = "stub_fixture_#{ExVCR.Util.uniq_id()}" 40 | stub = prepare_stub_records(unquote(options), adapter_method()) 41 | recorder = Recorder.start(fixture: stub_fixture, stub: stub, adapter: adapter_method()) 42 | 43 | try do 44 | mock_methods(recorder, adapter_method()) 45 | [do: return_value] = unquote(test) 46 | return_value 47 | after 48 | module_name = adapter_method().module_name() 49 | unload(module_name) 50 | ExVCR.MockLock.release_lock() 51 | end 52 | end 53 | end 54 | 55 | defmacro use_cassette(fixture, options, test) do 56 | quote do 57 | recorder = start_cassette(unquote(fixture), unquote(options)) 58 | 59 | try do 60 | [do: return_value] = unquote(test) 61 | return_value 62 | after 63 | stop_cassette(recorder) 64 | end 65 | end 66 | end 67 | 68 | defmacro use_cassette(fixture, test) do 69 | quote do 70 | use_cassette(unquote(fixture), [], unquote(test)) 71 | end 72 | end 73 | 74 | defmacro start_cassette(fixture, options) when fixture != :stub do 75 | quote do 76 | recorder = 77 | Recorder.start( 78 | unquote(options) ++ 79 | [fixture: normalize_fixture(unquote(fixture)), adapter: adapter_method()] 80 | ) 81 | 82 | mock_methods(recorder, adapter_method()) 83 | recorder 84 | end 85 | end 86 | 87 | defmacro stop_cassette(recorder) do 88 | quote do 89 | recorder_result = Recorder.save(unquote(recorder)) 90 | 91 | module_name = adapter_method().module_name() 92 | unload(module_name) 93 | ExVCR.MockLock.release_lock() 94 | 95 | recorder_result 96 | end 97 | end 98 | 99 | @doc false 100 | defp load(adapter, recorder) do 101 | if ExVCR.Application.global_mock_enabled?() do 102 | ExVCR.Actor.CurrentRecorder.set(recorder) 103 | else 104 | module_name = adapter.module_name() 105 | target_methods = adapter.target_methods(recorder) 106 | 107 | Enum.each(target_methods, fn {function, callback} -> 108 | :meck.expect(module_name, function, callback) 109 | end) 110 | end 111 | end 112 | 113 | @doc false 114 | def unload(module_name) do 115 | if ExVCR.Application.global_mock_enabled?() do 116 | ExVCR.Actor.CurrentRecorder.default_state() 117 | |> ExVCR.Actor.CurrentRecorder.set() 118 | else 119 | :meck.unload(module_name) 120 | end 121 | end 122 | 123 | @doc """ 124 | Mock methods pre-defined for the specified adapter. 125 | """ 126 | def mock_methods(recorder, adapter) do 127 | parent_pid = self() 128 | 129 | Task.async(fn -> 130 | ExVCR.MockLock.ensure_started() 131 | ExVCR.MockLock.request_lock(self(), parent_pid) 132 | 133 | receive do 134 | :lock_granted -> 135 | load(adapter, recorder) 136 | end 137 | end) 138 | |> Task.await(:infinity) 139 | end 140 | 141 | @doc """ 142 | Prepare stub records 143 | """ 144 | def prepare_stub_records(options, adapter) do 145 | if Keyword.keyword?(options) do 146 | prepare_stub_record(options, adapter) 147 | else 148 | Enum.flat_map(options, &prepare_stub_record(&1, adapter)) 149 | end 150 | end 151 | 152 | @doc """ 153 | Prepare stub record based on specified option parameters. 154 | """ 155 | def prepare_stub_record(options, adapter) do 156 | method = (options[:method] || "get") |> to_string() 157 | url = (options[:url] || "~r/.+/") |> to_string() 158 | body = (options[:body] || "Hello World") |> to_string() 159 | # REVIEW: would be great to have "~r/.+/" as default request_body 160 | request_body = (options[:request_body] || "") |> to_string() 161 | 162 | headers = options[:headers] || adapter.default_stub_params(:headers) 163 | status_code = options[:status_code] || adapter.default_stub_params(:status_code) 164 | 165 | record = %{ 166 | "request" => %{"method" => method, "url" => url, "request_body" => request_body}, 167 | "response" => %{"body" => body, "headers" => headers, "status_code" => status_code} 168 | } 169 | 170 | [adapter.convert_from_string(record)] 171 | end 172 | 173 | @doc """ 174 | Normalize fixture name for using as json file names, which removes whitespaces and align case. 175 | """ 176 | def normalize_fixture(fixture) do 177 | fixture |> String.replace(~r/\s/, "_") |> String.downcase() 178 | end 179 | end 180 | -------------------------------------------------------------------------------- /lib/exvcr/mock_lock.ex: -------------------------------------------------------------------------------- 1 | defmodule ExVCR.MockLock do 2 | use GenServer 3 | @ten_milliseconds 10 4 | 5 | def start() do 6 | GenServer.start(__MODULE__, %{lock_holder: nil}, name: :mock_lock) 7 | end 8 | 9 | def ensure_started do 10 | if !Process.whereis(:mock_lock) do 11 | __MODULE__.start() 12 | end 13 | end 14 | 15 | def request_lock(caller_pid, test_pid) do 16 | GenServer.cast(:mock_lock, {:request_lock, caller_pid, test_pid}) 17 | end 18 | 19 | def release_lock() do 20 | GenServer.call(:mock_lock, :release_lock) 21 | end 22 | 23 | # Callbacks 24 | 25 | @impl true 26 | def init(state) do 27 | {:ok, state} 28 | end 29 | 30 | @impl true 31 | def handle_cast({:request_lock, caller_pid, test_pid}, state) do 32 | Process.send(self(), {:do_request_lock, caller_pid, test_pid}, []) 33 | {:noreply, state} 34 | end 35 | 36 | @impl true 37 | def handle_info({:do_request_lock, caller_pid, test_pid}, state) do 38 | if Map.get(state, :lock_holder) do 39 | Process.send_after(self(), {:do_request_lock, caller_pid, test_pid}, @ten_milliseconds) 40 | {:noreply, state} 41 | else 42 | Process.monitor(test_pid) 43 | Process.send(caller_pid, :lock_granted, []) 44 | {:noreply, Map.put(state, :lock_holder, caller_pid)} 45 | end 46 | end 47 | 48 | @impl true 49 | def handle_info({:DOWN, _ref, :process, pid, _}, state) do 50 | if state.lock_holder == pid do 51 | {:noreply, Map.put(state, :lock_holder, nil)} 52 | else 53 | {:noreply, state} 54 | end 55 | end 56 | 57 | @impl true 58 | def handle_call(:release_lock, _from, state) do 59 | {:reply, :ok, Map.put(state, :lock_holder, nil)} 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /lib/exvcr/recorder.ex: -------------------------------------------------------------------------------- 1 | defmodule ExVCR.Recorder do 2 | @moduledoc """ 3 | Provides data saving/loading capability for HTTP interactions. 4 | """ 5 | 6 | alias ExVCR.Handler 7 | alias ExVCR.Actor.Responses 8 | alias ExVCR.Actor.Options 9 | 10 | @doc """ 11 | Initialize recorder. 12 | """ 13 | def start(options) do 14 | ExVCR.Checker.start([]) 15 | 16 | {:ok, act_responses} = Responses.start([]) 17 | {:ok, act_options} = Options.start(options) 18 | 19 | recorder = %ExVCR.Record{options: act_options, responses: act_responses} 20 | 21 | if stub = options(recorder)[:stub] do 22 | set(stub, recorder) 23 | else 24 | load_from_json(recorder) 25 | end 26 | 27 | recorder 28 | end 29 | 30 | @doc """ 31 | Provides entry point to be called from :meck library. HTTP request arguments are specified as args parameter. 32 | If response is not found in the cache, access to the server. 33 | Implementation for global mock. 34 | """ 35 | def request(request) do 36 | ExVCR.Actor.CurrentRecorder.get() 37 | |> request(request) 38 | end 39 | 40 | @doc """ 41 | Provides entry point to be called from :meck library. HTTP request arguments are specified as args parameter. 42 | If response is not found in the cache, access to the server. 43 | """ 44 | def request(recorder, request) do 45 | Handler.get_response(recorder, request) 46 | end 47 | 48 | @doc """ 49 | Load record-data from json file. 50 | """ 51 | def load_from_json(recorder) do 52 | file_path = get_file_path(recorder) 53 | custom_mode = options(recorder)[:custom] 54 | adapter = options(recorder)[:adapter] 55 | responses = ExVCR.JSON.load(file_path, custom_mode, adapter) 56 | set(responses, recorder) 57 | end 58 | 59 | @doc """ 60 | Save record-data into json file. 61 | """ 62 | def save(recorder) do 63 | file_path = get_file_path(recorder) 64 | 65 | if File.exists?(file_path) == false do 66 | ExVCR.JSON.save(file_path, ExVCR.Recorder.get(recorder)) 67 | end 68 | end 69 | 70 | @doc """ 71 | Returns the file path of the save/load target, based on the custom_mode(true or false). 72 | """ 73 | def get_file_path(recorder) do 74 | opts = options(recorder) 75 | 76 | directory = 77 | case opts[:custom] do 78 | true -> ExVCR.Setting.get(:custom_library_dir) 79 | _ -> ExVCR.Setting.get(:cassette_library_dir) 80 | end 81 | 82 | "#{directory}/#{opts[:fixture]}.json" 83 | end 84 | 85 | def options(recorder), do: Options.get(recorder.options) 86 | def get(recorder), do: Responses.get(recorder.responses) 87 | def set(responses, recorder), do: Responses.set(recorder.responses, responses) 88 | def append(recorder, x), do: Responses.append(recorder.responses, x) 89 | def pop(recorder), do: Responses.pop(recorder.responses) 90 | def update(recorder, finder, updater), do: Responses.update(recorder.responses, finder, updater) 91 | end 92 | -------------------------------------------------------------------------------- /lib/exvcr/records.ex: -------------------------------------------------------------------------------- 1 | defmodule ExVCR.Record do 2 | defstruct options: nil, responses: nil 3 | end 4 | 5 | defmodule ExVCR.Request do 6 | defstruct url: nil, headers: [], method: nil, body: nil, options: [], request_body: "" 7 | end 8 | 9 | defmodule ExVCR.Response do 10 | defstruct type: "ok", status_code: nil, headers: [], body: nil, binary: false 11 | end 12 | 13 | defmodule ExVCR.Checker.Results do 14 | defstruct dirs: nil, files: [] 15 | end 16 | 17 | defmodule ExVCR.Checker.Counts do 18 | defstruct server: 0, cache: 0 19 | end 20 | -------------------------------------------------------------------------------- /lib/exvcr/setting.ex: -------------------------------------------------------------------------------- 1 | defmodule ExVCR.Setting do 2 | @moduledoc """ 3 | An module to store the configuration settings. 4 | """ 5 | 6 | def get(key) do 7 | setup() 8 | :ets.lookup(table(), key)[key] 9 | end 10 | 11 | def set(key, value) do 12 | setup() 13 | :ets.insert(table(), {key, value}) 14 | end 15 | 16 | def append(key, value) do 17 | case __MODULE__.get(key) do 18 | [_ | _] = values -> __MODULE__.set(key, [value | values]) 19 | _ -> __MODULE__.set(key, [value]) 20 | end 21 | end 22 | 23 | defp setup do 24 | if :ets.info(table()) == :undefined do 25 | :ets.new(table(), [:set, :public, :named_table]) 26 | ExVCR.ConfigLoader.load_defaults() 27 | end 28 | end 29 | 30 | defp table do 31 | if Application.get_env(:exvcr, :enable_global_settings) do 32 | :exvcr_setting 33 | else 34 | :"exvcr_setting#{inspect(self())}" 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/exvcr/task/runner.ex: -------------------------------------------------------------------------------- 1 | defmodule ExVCR.Task.Runner do 2 | @moduledoc """ 3 | Provides task processing logics, which will be invoked by custom mix tasks. 4 | """ 5 | 6 | @print_header_format " ~-40s ~-30s\n" 7 | @check_header_format " ~-40s ~-20s ~-20s\n" 8 | @check_content_format " ~-40s ~-20w ~-20w\n" 9 | @date_format "~4..0B/~2..0B/~2..0B ~2..0B:~2..0B:~2..0B" 10 | @json_file_pattern ~r/\.json$/ 11 | 12 | @doc """ 13 | Use specified path to show the list of vcr cassettes. 14 | """ 15 | def show_vcr_cassettes(path_list) do 16 | Enum.each(path_list, fn path -> 17 | if File.exists?(path) do 18 | read_cassettes(path) |> print_cassettes(path) 19 | IO.puts("") 20 | end 21 | end) 22 | end 23 | 24 | defp read_cassettes(path) do 25 | file_names = find_json_files(path) 26 | date_times = Enum.map(file_names, &extract_last_modified_time(path, &1)) 27 | Enum.zip(file_names, date_times) 28 | end 29 | 30 | defp find_json_files(path) do 31 | if File.exists?(path) do 32 | File.ls!(path) 33 | |> Enum.filter(&(&1 =~ @json_file_pattern)) 34 | |> Enum.sort() 35 | else 36 | raise ExVCR.PathNotFoundError, message: "Specified path '#{path}' for reading cassettes was not found." 37 | end 38 | end 39 | 40 | defp extract_last_modified_time(path, file_name) do 41 | {{year, month, day}, {hour, min, sec}} = File.stat!(Path.join(path, file_name)).mtime 42 | sprintf(@date_format, [year, month, day, hour, min, sec]) 43 | end 44 | 45 | defp print_cassettes(items, path) do 46 | IO.puts("Showing list of cassettes in [#{path}]") 47 | printf(@print_header_format, ["[File Name]", "[Last Update]"]) 48 | Enum.each(items, fn {name, date} -> printf(@print_header_format, [name, date]) end) 49 | end 50 | 51 | @doc """ 52 | Use specified path to delete cassettes. 53 | """ 54 | def delete_cassettes(path, file_patterns, is_interactive \\ false) do 55 | path 56 | |> find_json_files() 57 | |> Enum.filter(&(&1 =~ file_patterns)) 58 | |> Enum.each(&delete_and_print_name(path, &1, is_interactive)) 59 | end 60 | 61 | defp delete_and_print_name(path, file_name, true) do 62 | line = IO.gets("delete #{file_name}? ") 63 | 64 | if String.upcase(line) == "Y\n" do 65 | delete_and_print_name(path, file_name, false) 66 | end 67 | end 68 | 69 | defp delete_and_print_name(path, file_name, false) do 70 | case Path.expand(file_name, path) |> File.rm() do 71 | :ok -> IO.puts("Deleted #{file_name}.") 72 | :error -> IO.puts("Failed to delete #{file_name}") 73 | end 74 | end 75 | 76 | @doc """ 77 | Check and show which cassettes are used by the test execution. 78 | """ 79 | def check_cassettes(record) do 80 | count_hash = create_count_hash(record.files, %{}) 81 | 82 | Enum.each(record.dirs, fn dir -> 83 | IO.puts("Showing hit counts of cassettes in [#{dir}]") 84 | 85 | if File.exists?(dir) do 86 | cassettes = read_cassettes(dir) 87 | print_check_cassettes(cassettes, count_hash) 88 | end 89 | 90 | IO.puts("") 91 | end) 92 | end 93 | 94 | defp create_count_hash([], acc), do: acc 95 | 96 | defp create_count_hash([{type, path} | tail], acc) do 97 | file = Path.basename(path) 98 | counts = Map.get(acc, file, %ExVCR.Checker.Counts{}) 99 | 100 | hash = 101 | case type do 102 | :cache -> Map.put(acc, file, %{counts | cache: counts.cache + 1}) 103 | :server -> Map.put(acc, file, %{counts | server: counts.server + 1}) 104 | end 105 | 106 | create_count_hash(tail, hash) 107 | end 108 | 109 | defp print_check_cassettes(items, counts_hash) do 110 | printf(@check_header_format, ["[File Name]", "[Cassette Counts]", "[Server Counts]"]) 111 | 112 | Enum.each(items, fn {name, _date} -> 113 | counts = Map.get(counts_hash, name, %ExVCR.Checker.Counts{}) 114 | printf(@check_content_format, [name, counts.cache, counts.server]) 115 | end) 116 | end 117 | 118 | defp printf(format, params) do 119 | IO.write(sprintf(format, params)) 120 | end 121 | 122 | defp sprintf(format, params) do 123 | char_list = :io_lib.format(format, params) 124 | List.to_string(char_list) 125 | end 126 | end 127 | -------------------------------------------------------------------------------- /lib/exvcr/task/show.ex: -------------------------------------------------------------------------------- 1 | defmodule ExVCR.Task.Show do 2 | @moduledoc """ 3 | Handles [mix vcr.show] task execution. 4 | """ 5 | 6 | @doc """ 7 | Displays the contents of cassettes. 8 | This method will called by the mix task. 9 | """ 10 | def run(files) do 11 | Enum.each(files, &print_file/1) 12 | end 13 | 14 | defp print_file(file) do 15 | if File.exists?(file) do 16 | IO.puts("\e[32mShowing #{file}\e[m") 17 | IO.puts("\e[32m**************************************\e[m") 18 | json = File.read!(file) 19 | IO.puts(json |> JSX.prettify!() |> String.replace(~r/\\n/, "\n")) 20 | display_parsed_body(json) 21 | IO.puts("\e[32m**************************************\e[m") 22 | else 23 | IO.puts("Specified file [#{file}] was not found.") 24 | end 25 | end 26 | 27 | defp display_parsed_body(json) do 28 | case extract_body(json) |> JSX.prettify() do 29 | {:ok, body_json} -> 30 | IO.puts("\n\e[33m[Showing parsed JSON body]\e[m") 31 | IO.puts(body_json) 32 | 33 | _ -> 34 | nil 35 | end 36 | end 37 | 38 | defp extract_body(json) do 39 | json 40 | |> JSX.decode!() 41 | |> List.first() 42 | |> Enum.into(%{}) 43 | |> get_in(["response", "body"]) 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/exvcr/task/util.ex: -------------------------------------------------------------------------------- 1 | defmodule ExVCR.Task.Util do 2 | @moduledoc """ 3 | Provides task related utilities. 4 | """ 5 | 6 | @doc """ 7 | Parse basic option parameters, which are commonly used by multiple mix tasks. 8 | """ 9 | def parse_basic_options(options) do 10 | [ 11 | options[:dir] || ExVCR.Setting.get(:cassette_library_dir), 12 | options[:custom] || ExVCR.Setting.get(:custom_library_dir) 13 | ] 14 | end 15 | 16 | @doc """ 17 | Method for printing help message. 18 | """ 19 | def print_help_message do 20 | IO.puts(""" 21 | Usage: mix vcr [options] 22 | Used to display the list of cassettes 23 | 24 | -h (--help) Show helps for vcr mix tasks 25 | -d (--dir) Specify vcr cassettes directory 26 | -c (--custom) Specify custom cassettes directory 27 | 28 | Usage: mix vcr.delete [options] [cassette-file-names] 29 | Used to delete cassettes 30 | 31 | -d (--dir) Specify vcr cassettes directory 32 | -c (--custom) Specify custom cassettes directory 33 | -i (--interactive) Request confirmation before attempting to delete 34 | -a (--all) Delete all the files by ignoring specified [filenames] 35 | 36 | Usage: mix vcr.check [options] [test-files] 37 | Used to check cassette use on test execution 38 | 39 | -d (--dir) Specify vcr cassettes directory 40 | -c (--custom) Specify custom cassettes directory 41 | 42 | Usage: mix vcr.show [cassette-file-names] 43 | Used to show cassette contents 44 | 45 | """) 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/exvcr/util.ex: -------------------------------------------------------------------------------- 1 | defmodule ExVCR.Util do 2 | @moduledoc """ 3 | Provides utility functions. 4 | """ 5 | 6 | @doc """ 7 | Returns uniq_id string based on current timestamp (ex. 1407237617115869) 8 | """ 9 | def uniq_id do 10 | :os.timestamp() |> Tuple.to_list() |> Enum.join("") 11 | end 12 | 13 | @doc """ 14 | Takes a keyword lists and returns them as strings. 15 | """ 16 | 17 | def stringify_keys(list) do 18 | list |> Enum.map(fn {key, value} -> {to_string(key), to_string(value)} end) 19 | end 20 | 21 | def build_url(scheme, host, path, port \\ nil, query \\ nil) do 22 | scheme = 23 | case scheme do 24 | s when s in [:http, "http", "HTTP"] -> "http://" 25 | s when s in [:https, "https", "HTTPS"] -> "https://" 26 | _ -> scheme 27 | end 28 | 29 | port = 30 | cond do 31 | scheme == "http://" && port == 80 -> nil 32 | scheme == "https://" && port == 443 -> nil 33 | true -> port 34 | end 35 | 36 | url = 37 | if port do 38 | "#{scheme}#{host}:#{port}#{path}" 39 | else 40 | "#{scheme}#{host}#{path}" 41 | end 42 | 43 | url = 44 | if query != nil && query != "" do 45 | "#{url}?#{query}" 46 | else 47 | url 48 | end 49 | 50 | url 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/mix/tasks.ex: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.Vcr do 2 | use Mix.Task 3 | 4 | @shortdoc "Operate exvcr cassettes" 5 | 6 | @moduledoc """ 7 | Provides mix tasks for operating cassettes. 8 | 9 | ## Command line options 10 | * `--dir` - specifies the vcr cassette directory. 11 | * `--custom` - specifies the custom cassette directory. 12 | * `-i (--interactive)` - ask for confirmation for each file operation. 13 | """ 14 | 15 | @doc "Entry point for [mix vcr] task" 16 | def run(args) do 17 | {options, _, _} = 18 | OptionParser.parse(args, 19 | aliases: [d: :dir, c: :custom, h: :help], 20 | switches: [dir: :string, custom: :string, help: :boolean] 21 | ) 22 | 23 | if options[:help] do 24 | ExVCR.Task.Util.print_help_message() 25 | else 26 | ExVCR.Task.Util.parse_basic_options(options) |> ExVCR.Task.Runner.show_vcr_cassettes() 27 | end 28 | end 29 | 30 | defmodule Delete do 31 | use Mix.Task 32 | 33 | @doc "Entry point for [mix vcr.delete] task" 34 | def run(args) do 35 | {options, files, _} = 36 | OptionParser.parse(args, 37 | switches: [interactive: :boolean, all: :boolean], 38 | aliases: [d: :dir, i: :interactive, a: :all] 39 | ) 40 | 41 | pattern = 42 | cond do 43 | options[:all] -> ~r/.*/ 44 | Enum.count(files) == 1 -> Enum.at(files, 0) 45 | true -> nil 46 | end 47 | 48 | if pattern do 49 | ExVCR.Task.Runner.delete_cassettes( 50 | options[:dir] || ExVCR.Setting.get(:cassette_library_dir), 51 | pattern, 52 | options[:interactive] || false 53 | ) 54 | else 55 | IO.puts( 56 | "[Invalid Param] Specify substring of cassette file-name to be deleted - `mix vcr.delete [pattern]`, or use `mix vcr.delete --all` for deleting all cassettes." 57 | ) 58 | end 59 | end 60 | end 61 | 62 | defmodule Check do 63 | @moduledoc """ 64 | Check how the recorded cassettes are used while executing [mix test] task. 65 | """ 66 | use Mix.Task 67 | 68 | @doc "Entry point for [mix vcr.check] task." 69 | def run(args) do 70 | {options, _files, _} = 71 | OptionParser.parse(args, aliases: [d: :dir, c: :custom], switches: [dir: :string, custom: :string]) 72 | 73 | dirs = ExVCR.Task.Util.parse_basic_options(options) 74 | ExVCR.Checker.start(%ExVCR.Checker.Results{dirs: dirs}) 75 | 76 | Mix.env(:test) 77 | Mix.Task.run("test") 78 | 79 | System.at_exit(fn _ -> 80 | ExVCR.Task.Runner.check_cassettes(ExVCR.Checker.get()) 81 | end) 82 | end 83 | end 84 | 85 | defmodule Show do 86 | @moduledoc """ 87 | Show the contents of the cassettes. 88 | """ 89 | use Mix.Task 90 | 91 | @doc "Entry point for [mix vcr.show] task." 92 | def run(args) do 93 | {_options, files, _} = OptionParser.parse(args, aliases: [], switches: []) 94 | ExVCR.Task.Show.run(files) 95 | end 96 | end 97 | end 98 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule ExVCR.Mixfile do 2 | use Mix.Project 3 | 4 | @source_url "https://github.com/parroty/exvcr" 5 | @version "0.17.1" 6 | 7 | def project do 8 | [ 9 | app: :exvcr, 10 | version: @version, 11 | source_url: @source_url, 12 | elixir: "~> 1.3", 13 | deps: deps(), 14 | docs: docs(), 15 | description: description(), 16 | package: package(), 17 | test_coverage: [tool: ExCoveralls] 18 | ] 19 | end 20 | 21 | def cli do 22 | [preferred_envs: [coveralls: :test]] 23 | end 24 | 25 | def application do 26 | [ 27 | extra_applications: extra_applications(Mix.env()), 28 | mod: {ExVCR.Application, []} 29 | ] 30 | end 31 | 32 | defp extra_applications(:test), do: common_extra_applications() 33 | defp extra_applications(:dev), do: common_extra_applications() 34 | defp extra_applications(_), do: [] 35 | 36 | defp common_extra_applications do 37 | [ 38 | :inets, 39 | :ranch, 40 | :telemetry, 41 | :finch, 42 | :ibrowse, 43 | :hackney, 44 | :http_server, 45 | :httpoison, 46 | :excoveralls 47 | ] 48 | end 49 | 50 | def deps do 51 | [ 52 | {:meck, "~> 1.0"}, 53 | {:exjsx, "~> 4.0"}, 54 | {:ibrowse, "~> 4.4", optional: true}, 55 | {:httpoison, "~> 1.0 or ~> 2.0", optional: true}, 56 | {:finch, "~> 0.16", optional: true}, 57 | {:excoveralls, "~> 0.18", only: :test}, 58 | {:http_server, github: "parroty/http_server", only: [:dev, :test]}, 59 | {:ex_doc, ">= 0.0.0", only: :dev} 60 | ] 61 | end 62 | 63 | defp description do 64 | """ 65 | HTTP request/response recording library for elixir, inspired by VCR. 66 | """ 67 | end 68 | 69 | defp package do 70 | [ 71 | maintainers: ["parroty"], 72 | licenses: ["MIT"], 73 | links: %{ 74 | "GitHub" => @source_url, 75 | "Changelog" => "#{@source_url}/blob/master/CHANGELOG.md" 76 | } 77 | ] 78 | end 79 | 80 | defp docs do 81 | [ 82 | main: "readme", 83 | source_ref: "v#{@version}", 84 | source_url: @source_url, 85 | extras: [ 86 | "CHANGELOG.md", 87 | "README.md" 88 | ] 89 | ] 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /package.exs: -------------------------------------------------------------------------------- 1 | Expm.Package.new(name: "exvcr", description: "Record and replay HTTP interactions library for elixir", 2 | version: "0.1.1", keywords: ["Elixir","vcr","http", "mock"], 3 | maintainers: [[name: "parroty", email: "parroty00@gmail.com"]], 4 | repositories: [[github: "parroty/exvcr"]]) 5 | -------------------------------------------------------------------------------- /test/adapter_httpc_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExVCR.Adapter.HttpcTest do 2 | use ExUnit.Case, async: true 3 | use ExVCR.Mock, adapter: ExVCR.Adapter.Httpc 4 | 5 | @port 34010 6 | 7 | setup_all do 8 | HttpServer.start(path: "/server", port: @port, response: "test_response") 9 | Application.ensure_started(:inets) 10 | 11 | on_exit(fn -> 12 | HttpServer.stop(@port) 13 | end) 14 | 15 | :ok 16 | end 17 | 18 | test "passthrough works when CurrentRecorder has an initial state" do 19 | if ExVCR.Application.global_mock_enabled?() do 20 | ExVCR.Actor.CurrentRecorder.default_state() 21 | |> ExVCR.Actor.CurrentRecorder.set() 22 | end 23 | 24 | url = "http://localhost:#{@port}/server" |> to_charlist() 25 | {:ok, result} = :httpc.request(url) 26 | {{_http_version, status_code, _reason_phrase}, _headers, _body} = result 27 | assert status_code == 200 28 | end 29 | 30 | test "passthrough works after cassette has been used" do 31 | url = "http://localhost:#{@port}/server" |> to_charlist() 32 | 33 | use_cassette "httpc_get_localhost" do 34 | {:ok, result} = :httpc.request(url) 35 | {{_http_version, status_code, _reason_phrase}, _headers, _body} = result 36 | assert status_code == 200 37 | end 38 | 39 | {:ok, result} = :httpc.request(url) 40 | {{_http_version, status_code, _reason_phrase}, _headers, _body} = result 41 | assert status_code == 200 42 | end 43 | 44 | test "example httpc request/1" do 45 | use_cassette "example_httpc_request_1" do 46 | {:ok, result} = :httpc.request(~c"http://example.com") 47 | {{http_version, _status_code = 200, reason_phrase}, headers, body} = result 48 | assert to_string(body) =~ ~r/Example Domain/ 49 | assert http_version == ~c"HTTP/1.1" 50 | assert reason_phrase == ~c"OK" 51 | assert List.keyfind(headers, ~c"content-type", 0) == {~c"content-type", ~c"text/html"} 52 | end 53 | end 54 | 55 | test "example httpc request/4" do 56 | use_cassette "example_httpc_request_4" do 57 | {:ok, {{_, 200, _reason_phrase}, _headers, body}} = 58 | :httpc.request(:get, {~c"http://example.com", ~c""}, ~c"", ~c"") 59 | 60 | assert to_string(body) =~ ~r/Example Domain/ 61 | end 62 | end 63 | 64 | test "example httpc request/4 with additional options" do 65 | use_cassette "example_httpc_request_4_additional_options" do 66 | {:ok, {{_, 200, _reason_phrase}, _headers, body}} = 67 | :httpc.request( 68 | :get, 69 | {~c"http://example.com", [{~c"Content-Type", ~c"text/html"}]}, 70 | [connect_timeout: 3000, timeout: 5000], 71 | body_format: :binary 72 | ) 73 | 74 | assert to_string(body) =~ ~r/Example Domain/ 75 | end 76 | end 77 | 78 | test "example httpc request error" do 79 | use_cassette "example_httpc_request_error" do 80 | {:error, {reason, _detail}} = :httpc.request(~c"http://invalidurl") 81 | assert reason == :failed_connect 82 | end 83 | end 84 | 85 | test "stub request works" do 86 | use_cassette :stub, url: ~c"http://example.com", body: ~c"Stub Response" do 87 | {:ok, result} = :httpc.request(~c"http://example.com") 88 | {{_http_version, _status_code = 200, _reason_phrase}, headers, body} = result 89 | assert to_string(body) =~ ~r/Stub Response/ 90 | assert List.keyfind(headers, ~c"content-type", 0) == {~c"content-type", ~c"text/html"} 91 | end 92 | end 93 | end 94 | -------------------------------------------------------------------------------- /test/adapter_ibrowse_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExVCR.Adapter.IBrowseTest do 2 | use ExUnit.Case, async: true 3 | use ExVCR.Mock 4 | 5 | @port 34011 6 | 7 | setup_all do 8 | HttpServer.start(path: "/server", port: @port, response: "test_response") 9 | Application.ensure_started(:ibrowse) 10 | 11 | on_exit(fn -> 12 | HttpServer.stop(@port) 13 | end) 14 | 15 | :ok 16 | end 17 | 18 | test "passthrough works when CurrentRecorder has an initial state" do 19 | if ExVCR.Application.global_mock_enabled?() do 20 | ExVCR.Actor.CurrentRecorder.default_state() 21 | |> ExVCR.Actor.CurrentRecorder.set() 22 | end 23 | 24 | url = "http://localhost:#{@port}/server" |> to_charlist() 25 | {:ok, status_code, _headers, _body} = :ibrowse.send_req(url, [], :get) 26 | assert status_code == ~c"200" 27 | end 28 | 29 | test "passthrough works after cassette has been used" do 30 | url = "http://localhost:#{@port}/server" |> to_charlist() 31 | 32 | use_cassette "ibrowse_get_localhost" do 33 | {:ok, status_code, _headers, _body} = :ibrowse.send_req(url, [], :get) 34 | assert status_code == ~c"200" 35 | end 36 | 37 | {:ok, status_code, _headers, _body} = :ibrowse.send_req(url, [], :get) 38 | assert status_code == ~c"200" 39 | end 40 | 41 | test "example single request" do 42 | use_cassette "example_ibrowse" do 43 | {:ok, status_code, headers, body} = :ibrowse.send_req(~c"http://example.com", [], :get) 44 | assert status_code == ~c"200" 45 | assert List.keyfind(headers, ~c"Content-Type", 0) == {~c"Content-Type", ~c"text/html"} 46 | assert to_string(body) =~ ~r/Example Domain/ 47 | end 48 | end 49 | 50 | test "example multiple requests" do 51 | use_cassette "example_ibrowse_multiple" do 52 | {:ok, status_code, _headers, body} = :ibrowse.send_req(~c"http://example.com", [], :get) 53 | assert status_code == ~c"200" 54 | assert to_string(body) =~ ~r/Example Domain/ 55 | 56 | {:ok, status_code, _headers, body} = :ibrowse.send_req(~c"http://example.com/2", [], :get) 57 | assert status_code == ~c"404" 58 | assert to_string(body) =~ ~r/Example Domain/ 59 | end 60 | end 61 | 62 | test "single request with error" do 63 | use_cassette "error_ibrowse" do 64 | response = :ibrowse.send_req(~c"http://invalid_url", [], :get) 65 | assert response == {:error, {:conn_failed, {:error, :nxdomain}}} 66 | end 67 | end 68 | 69 | test "using recorded cassette, but requesting with different url should return error" do 70 | use_cassette "example_ibrowse_different" do 71 | {:ok, status_code, _headers, body} = :ibrowse.send_req(~c"http://example.com", [], :get) 72 | assert status_code == ~c"200" 73 | assert to_string(body) =~ ~r/Example Domain/ 74 | end 75 | 76 | use_cassette "example_ibrowse_different" do 77 | assert_raise ExVCR.RequestNotMatchError, ~r/different_from_original/, fn -> 78 | :ibrowse.send_req(~c"http://example.com/different_from_original", [], :get) 79 | end 80 | end 81 | end 82 | 83 | test "stub request works for ibrowse" do 84 | use_cassette :stub, url: ~c"http://example.com", body: ~c"Stub Response", status_code: 200 do 85 | {:ok, status_code, _headers, body} = :ibrowse.send_req(~c"http://example.com", [], :get) 86 | assert status_code == ~c"200" 87 | assert to_string(body) =~ ~r/Stub Response/ 88 | end 89 | end 90 | 91 | test "stub multiple requests works for ibrowse" do 92 | stubs = [ 93 | [url: "http://example.com/1", body: "Stub Response 1", status_code: 200], 94 | [url: "http://example.com/2", body: "Stub Response 2", status_code: 404] 95 | ] 96 | 97 | use_cassette :stub, stubs do 98 | {:ok, status_code, _headers, body} = :ibrowse.send_req(~c"http://example.com/1", [], :get) 99 | assert status_code == ~c"200" 100 | assert to_string(body) =~ ~r/Stub Response 1/ 101 | 102 | {:ok, status_code, _headers, body} = :ibrowse.send_req(~c"http://example.com/2", [], :get) 103 | assert status_code == ~c"404" 104 | assert to_string(body) =~ ~r/Stub Response 2/ 105 | end 106 | end 107 | end 108 | -------------------------------------------------------------------------------- /test/cassettes/test1.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "url": "http://example.com", 5 | "headers": [], 6 | "method": "get", 7 | "body": "", 8 | "options": [] 9 | }, 10 | "response": { 11 | "status_code": 200, 12 | "headers": { 13 | "Date": "Mon, 1 Jan 2013 00:00:00 GMT" 14 | }, 15 | "body": "\n\n\n Example Domain\n\n \n \n \n \n\n\n\n
\n

Example Domain

\n

This domain is established to be used for illustrative examples in documents. You may use this\n domain in examples without prior coordination or asking for permission.

\n

More information...

\n
\n\n\n" 16 | } 17 | } 18 | ] -------------------------------------------------------------------------------- /test/cassettes/test2.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "url": "http://example.com", 5 | "headers": [], 6 | "method": "get", 7 | "body": "", 8 | "options": [] 9 | }, 10 | "response": { 11 | "status_code": 200, 12 | "headers": { 13 | "Date": "Mon, 2 Jan 2013 00:00:00 GMT" 14 | }, 15 | "body": "\n\n\n Example Domain\n\n \n \n \n \n\n\n\n
\n

Example Domain

\n

This domain is established to be used for illustrative examples in documents. You may use this\n domain in examples without prior coordination or asking for permission.

\n

More information...

\n
\n\n\n" 16 | } 17 | } 18 | ] -------------------------------------------------------------------------------- /test/config_loader_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExVCR.ConfigLoaderTest do 2 | use ExUnit.Case, async: true 3 | 4 | @dummy_cassette_dir "tmp/vcr_tmp/vcr_cassettes" 5 | @dummy_custom_dir "tmp/vcr_tmp/vcr_custom" 6 | 7 | setup_all do 8 | File.rm_rf!(@dummy_cassette_dir) 9 | :ok 10 | end 11 | 12 | setup do 13 | on_exit(fn -> 14 | File.rm_rf!(@dummy_cassette_dir) 15 | :ok 16 | end) 17 | 18 | :ok 19 | end 20 | 21 | test "loading default setting from config.exs" do 22 | # Set dummy values 23 | ExVCR.Config.cassette_library_dir(@dummy_cassette_dir, @dummy_custom_dir) 24 | ExVCR.Config.filter_sensitive_data("test_before1", "test_after1") 25 | ExVCR.Config.filter_url_params(true) 26 | ExVCR.Config.response_headers_blacklist(["Content-Type", "Accept"]) 27 | 28 | # Load default values (defined in config/config.exs) 29 | ExVCR.ConfigLoader.load_defaults() 30 | 31 | # Verify against default values 32 | assert ExVCR.Setting.get(:cassette_library_dir) == "fixture/vcr_cassettes" 33 | assert ExVCR.Setting.get(:custom_library_dir) == "fixture/custom_cassettes" 34 | assert ExVCR.Setting.get(:filter_url_params) == false 35 | assert ExVCR.Setting.get(:response_headers_blacklist) == [] 36 | end 37 | 38 | test "loading default setting from empty values" do 39 | # Backup current env values 40 | vcr_cassette_library_dir = Application.get_env(:exvcr, :vcr_cassette_library_dir) 41 | custom_cassette_library_dir = Application.get_env(:exvcr, :custom_cassette_library_dir) 42 | filter_sensitive_data = Application.get_env(:exvcr, :filter_sensitive_data) 43 | response_headers_blacklist = Application.get_env(:exvcr, :response_headers_blacklist) 44 | 45 | # Remove env values 46 | Application.delete_env(:exvcr, :vcr_cassette_library_dir) 47 | Application.delete_env(:exvcr, :custom_cassette_library_dir) 48 | Application.delete_env(:exvcr, :filter_sensitive_data) 49 | Application.delete_env(:exvcr, :response_headers_blacklist) 50 | 51 | # Load default values 52 | ExVCR.ConfigLoader.load_defaults() 53 | 54 | # Verify against default values 55 | assert ExVCR.Setting.get(:cassette_library_dir) == "fixture/vcr_cassettes" 56 | assert ExVCR.Setting.get(:custom_library_dir) == "fixture/custom_cassettes" 57 | assert ExVCR.Setting.get(:filter_url_params) == false 58 | assert ExVCR.Setting.get(:response_headers_blacklist) == [] 59 | 60 | # Restore env values 61 | Application.put_env(:exvcr, :vcr_cassette_library_dir, vcr_cassette_library_dir) 62 | Application.put_env(:exvcr, :custom_cassette_library_dir, custom_cassette_library_dir) 63 | Application.get_env(:exvcr, :filter_sensitive_data, filter_sensitive_data) 64 | Application.get_env(:exvcr, :response_headers_blacklist, response_headers_blacklist) 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /test/config_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExVCR.ConfigTest do 2 | use ExUnit.Case, async: true 3 | 4 | @dummy_cassette_dir "tmp/vcr_tmp/vcr_cassettes" 5 | @dummy_custom_dir "tmp/vcr_tmp/vcr_custom" 6 | 7 | setup_all do 8 | File.rm_rf!(@dummy_cassette_dir) 9 | :ok 10 | end 11 | 12 | setup do 13 | on_exit(fn -> 14 | File.rm_rf!(@dummy_cassette_dir) 15 | :ok 16 | end) 17 | 18 | :ok 19 | end 20 | 21 | test "setting up cassette library dir" do 22 | ExVCR.Config.cassette_library_dir(@dummy_cassette_dir) 23 | assert ExVCR.Setting.get(:cassette_library_dir) == @dummy_cassette_dir 24 | assert ExVCR.Setting.get(:custom_library_dir) == nil 25 | end 26 | 27 | test "setting up cassette and custom library dir" do 28 | ExVCR.Config.cassette_library_dir(@dummy_cassette_dir, @dummy_custom_dir) 29 | assert ExVCR.Setting.get(:cassette_library_dir) == @dummy_cassette_dir 30 | assert ExVCR.Setting.get(:custom_library_dir) == @dummy_custom_dir 31 | end 32 | 33 | test "add filter sensitive data" do 34 | ExVCR.Config.filter_sensitive_data(nil) 35 | ExVCR.Config.filter_sensitive_data("test_before1", "test_after1") 36 | ExVCR.Config.filter_sensitive_data("test_before2", "test_after2") 37 | 38 | assert ExVCR.Setting.get(:filter_sensitive_data) == 39 | [{"test_before2", "test_after2"}, {"test_before1", "test_after1"}] 40 | 41 | ExVCR.Config.filter_sensitive_data(nil) 42 | assert ExVCR.Setting.get(:filter_sensitive_data) == [] 43 | end 44 | 45 | test "add filter_url_params" do 46 | ExVCR.Config.filter_url_params(true) 47 | assert ExVCR.Setting.get(:filter_url_params) == true 48 | end 49 | 50 | test "add response headers blacklist" do 51 | ExVCR.Config.response_headers_blacklist(["Content-Type", "Accept"]) 52 | assert ExVCR.Setting.get(:response_headers_blacklist) == ["content-type", "accept"] 53 | 54 | ExVCR.Config.response_headers_blacklist([]) 55 | assert ExVCR.Setting.get(:response_headers_blacklist) == [] 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /test/enable_global_settings_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExVCR.EnableGlobalSettingsTest do 2 | use ExVCR.Mock 3 | use ExUnit.Case, async: false 4 | 5 | test "settings are normally per-process" do 6 | original_value = ExVCR.Setting.get(:custom_library_dir) 7 | 8 | ExVCR.Setting.set(:custom_library_dir, "global_setting_test") 9 | 10 | setting_from_task = Task.async(fn -> ExVCR.Setting.get(:custom_library_dir) end) 11 | 12 | assert Task.await(setting_from_task) == original_value 13 | assert ExVCR.Setting.get(:custom_library_dir) == "global_setting_test" 14 | end 15 | 16 | test "settings are shared globally among processes" do 17 | original_setting = Application.get_env(:exvcr, :enable_global_settings) 18 | 19 | Application.put_env(:exvcr, :enable_global_settings, true) 20 | 21 | ExVCR.Setting.set(:custom_library_dir, "global_setting_test") 22 | 23 | setting_from_task = Task.async(fn -> ExVCR.Setting.get(:custom_library_dir) end) 24 | 25 | assert Task.await(setting_from_task) == "global_setting_test" 26 | 27 | Application.put_env(:exvcr, :enable_global_settings, original_setting) 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /test/filter_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExVCR.FilterTest do 2 | use ExUnit.Case, async: true 3 | 4 | test "filter_sensitive_data" do 5 | ExVCR.Config.filter_sensitive_data(".+", "PLACEHOLDER") 6 | ExVCR.Config.filter_sensitive_data("secret", "PLACEHOLDER") 7 | 8 | content = "fooI have a secret" 9 | 10 | assert ExVCR.Filter.filter_sensitive_data(content) == 11 | "PLACEHOLDERI have a PLACEHOLDER" 12 | 13 | ExVCR.Config.filter_sensitive_data(nil) 14 | end 15 | 16 | test "filter_sensitive_data handles non string values" do 17 | assert ExVCR.Filter.filter_sensitive_data(60_000) == 60000 18 | end 19 | 20 | test "filter_url_params" do 21 | url = "https://example.com/api?test1=foo&test2=bar" 22 | 23 | ExVCR.Config.filter_url_params(true) 24 | ExVCR.Config.filter_sensitive_data("example.com", "example.org") 25 | ExVCR.Config.filter_sensitive_data("foo", "PLACEHOLDER") 26 | 27 | assert ExVCR.Filter.filter_url_params(url) == "https://example.org/api" 28 | 29 | ExVCR.Config.filter_url_params(false) 30 | 31 | assert ExVCR.Filter.filter_url_params(url) == 32 | "https://example.org/api?test1=PLACEHOLDER&test2=bar" 33 | 34 | ExVCR.Config.filter_sensitive_data(nil) 35 | end 36 | 37 | test "strip_query_params" do 38 | url = "https://example.com/api?test1=foo&test2=bar" 39 | assert ExVCR.Filter.strip_query_params(url) == "https://example.com/api" 40 | 41 | url = "https://example.com?test1=foo&test2=bar" 42 | assert ExVCR.Filter.strip_query_params(url) == "https://example.com" 43 | end 44 | 45 | test "remove_blacklisted_headers" do 46 | assert ExVCR.Filter.remove_blacklisted_headers([]) == [] 47 | 48 | ExVCR.Config.response_headers_blacklist(["X-Filter1", "X-Filter2"]) 49 | headers = [{"X-Filter1", "1"}, {"x-filter2", "2"}, {"X-NoFilter", "3"}] 50 | filtered_headers = ExVCR.Filter.remove_blacklisted_headers(headers) 51 | assert filtered_headers == [{"X-NoFilter", "3"}] 52 | 53 | ExVCR.Config.response_headers_blacklist([]) 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /test/handler_custom_mode_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExVCR.Adapter.HandlerCustomModeTest do 2 | use ExUnit.Case, async: true 3 | use ExVCR.Mock, adapter: ExVCR.Adapter.Hackney 4 | 5 | test "query param match succeeds with custom mode" do 6 | use_cassette "response_mocking_with_param", custom: true do 7 | HTTPoison.get!("http://example.com?another_param=456&auth_token=123abc", []).body =~ ~r/Custom Response/ 8 | end 9 | end 10 | 11 | test "custom with valid response" do 12 | use_cassette "response_mocking", custom: true do 13 | assert HTTPoison.get!("http://example.com", []).body =~ ~r/Custom Response/ 14 | end 15 | end 16 | 17 | test "custom response with regexp url" do 18 | use_cassette "response_mocking_regex", custom: true do 19 | HTTPoison.get!("http://example.com/something/abc", []).body =~ ~r/Custom Response/ 20 | end 21 | end 22 | 23 | test "custom without valid response throws error" do 24 | assert_raise ExVCR.InvalidRequestError, fn -> 25 | use_cassette "response_mocking", custom: true do 26 | HTTPoison.get!("http://invalidurl.example.com/invalid", []) 27 | end 28 | end 29 | end 30 | 31 | test "custom without valid response file throws error" do 32 | assert_raise ExVCR.FileNotFoundError, fn -> 33 | use_cassette "invalid_file_response", custom: true do 34 | HTTPoison.get!("http://example.com", []) 35 | end 36 | end 37 | end 38 | 39 | test "match method succeeds" do 40 | use_cassette "method_mocking", custom: true do 41 | HTTPoison.post!("http://example.com", []).body =~ ~r/Custom Response/ 42 | end 43 | end 44 | 45 | test "match method fails" do 46 | assert_raise ExVCR.InvalidRequestError, fn -> 47 | use_cassette "method_mocking", custom: true do 48 | HTTPoison.put!("http://example.com", []).body =~ ~r/Custom Response/ 49 | end 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /test/handler_stub_mode_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExVCR.Adapter.HandlerStubModeTest do 2 | use ExUnit.Case, async: true 3 | use ExVCR.Mock 4 | 5 | setup_all do 6 | Application.ensure_started(:ibrowse) 7 | :ok 8 | end 9 | 10 | test "empty options works with default parameters" do 11 | use_cassette :stub, [] do 12 | {:ok, status_code, headers, body} = :ibrowse.send_req(~c"http://localhost", [], :get) 13 | assert status_code == ~c"200" 14 | assert List.keyfind(headers, ~c"Content-Type", 0) == {~c"Content-Type", ~c"text/html"} 15 | assert to_string(body) =~ ~r/Hello World/ 16 | end 17 | end 18 | 19 | test "specified options should match with return values" do 20 | use_cassette :stub, url: ~c"http://localhost", body: ~c"NotFound", status_code: 404 do 21 | {:ok, status_code, _headers, body} = :ibrowse.send_req(~c"http://localhost", [], :get) 22 | assert status_code == ~c"404" 23 | assert to_string(body) =~ ~r/NotFound/ 24 | end 25 | end 26 | 27 | test "method name in atom works" do 28 | use_cassette :stub, url: ~c"http://localhost", method: :post, request_body: ~c"param1=value1¶m2=value2" do 29 | {:ok, status_code, _headers, _body} = 30 | :ibrowse.send_req(~c"http://localhost", [], :post, ~c"param1=value1¶m2=value2") 31 | 32 | assert status_code == ~c"200" 33 | end 34 | end 35 | 36 | test "url matches as regardless of query param order" do 37 | use_cassette :stub, url: "http://localhost?param1=10¶m2=20¶m3=30" do 38 | {:ok, status_code, _headers, body} = 39 | :ibrowse.send_req(~c"http://localhost?param3=30¶m1=10¶m2=20", [], :get) 40 | 41 | assert status_code == ~c"200" 42 | assert to_string(body) =~ ~r/Hello World/ 43 | end 44 | end 45 | 46 | test "url matches as regex" do 47 | use_cassette :stub, url: "~r/.+/" do 48 | {:ok, status_code, _headers, body} = :ibrowse.send_req(~c"http://localhost", [], :get) 49 | assert status_code == ~c"200" 50 | assert to_string(body) =~ ~r/Hello World/ 51 | end 52 | end 53 | 54 | test "request_body matches as string" do 55 | use_cassette :stub, url: ~c"http://localhost", method: :post, request_body: "some-string", body: "Hello World" do 56 | {:ok, status_code, _headers, body} = :ibrowse.send_req(~c"http://localhost", [], :post, ~c"some-string") 57 | assert status_code == ~c"200" 58 | assert to_string(body) =~ ~r/Hello World/ 59 | end 60 | end 61 | 62 | test "request_body matches as regex" do 63 | use_cassette :stub, url: ~c"http://localhost", method: :post, request_body: "~r/param1/", body: "Hello World" do 64 | {:ok, status_code, _headers, body} = 65 | :ibrowse.send_req(~c"http://localhost", [], :post, ~c"param1=value1¶m2=value2") 66 | 67 | assert status_code == ~c"200" 68 | assert to_string(body) =~ ~r/Hello World/ 69 | end 70 | end 71 | 72 | test "request_body mismatches as regex" do 73 | assert_raise ExVCR.InvalidRequestError, fn -> 74 | use_cassette :stub, url: ~c"http://localhost", method: :post, request_body: "~r/param3/", body: "Hello World" do 75 | {:ok, _status_code, _headers, _body} = 76 | :ibrowse.send_req(~c"http://localhost", [], :post, ~c"param1=value1¶m2=value2") 77 | end 78 | end 79 | end 80 | 81 | test "request_body matches as unordered list of params" do 82 | use_cassette :stub, 83 | url: ~c"http://localhost", 84 | method: :post, 85 | request_body: "param1=10¶m3=30¶m2=20", 86 | body: "Hello World" do 87 | {:ok, status_code, _headers, body} = 88 | :ibrowse.send_req(~c"http://localhost", [], :post, ~c"param2=20¶m1=10¶m3=30") 89 | 90 | assert status_code == ~c"200" 91 | assert to_string(body) =~ ~r/Hello World/ 92 | end 93 | end 94 | 95 | test "request_body mismatches as unordered list of params" do 96 | assert_raise ExVCR.InvalidRequestError, fn -> 97 | use_cassette :stub, 98 | url: ~c"http://localhost", 99 | method: :post, 100 | request_body: "param1=10¶m3=30¶m4=40", 101 | body: "Hello World" do 102 | {:ok, _status_code, _headers, _body} = 103 | :ibrowse.send_req(~c"http://localhost", [], :post, ~c"param2=20¶m1=10¶m3=30") 104 | end 105 | end 106 | end 107 | 108 | test "request_body mismatch should raise error" do 109 | assert_raise ExVCR.InvalidRequestError, fn -> 110 | use_cassette :stub, url: ~c"http://localhost", method: :post, request_body: ~c'{"one" => 1}' do 111 | {:ok, _status_code, _headers, _body} = :ibrowse.send_req(~c"http://localhost", [], :post) 112 | end 113 | end 114 | end 115 | 116 | test "post request without request_body definition should ignore request body" do 117 | use_cassette :stub, url: ~c"http://localhost", method: :post, status_code: 500 do 118 | {:ok, status_code, _headers, _body} = 119 | :ibrowse.send_req(~c"http://localhost", [], :post, ~c"param=should_be_ignored") 120 | 121 | assert status_code == ~c"500" 122 | end 123 | end 124 | 125 | test "url mismatch should raise error" do 126 | assert_raise ExVCR.InvalidRequestError, fn -> 127 | use_cassette :stub, url: ~c"http://localhost" do 128 | {:ok, _status_code, _headers, _body} = :ibrowse.send_req(~c"http://www.example.com", [], :get) 129 | end 130 | end 131 | end 132 | 133 | test "method mismatch should raise error" do 134 | assert_raise ExVCR.InvalidRequestError, fn -> 135 | use_cassette :stub, url: ~c"http://localhost", method: "post" do 136 | {:ok, _status_code, _headers, _body} = :ibrowse.send_req(~c"http://localhost", [], :get) 137 | end 138 | end 139 | end 140 | end 141 | -------------------------------------------------------------------------------- /test/iex_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExVCR.IExTest do 2 | use ExUnit.Case, async: true 3 | import ExUnit.CaptureIO 4 | require ExVCR.IEx 5 | 6 | @port 34005 7 | 8 | setup_all do 9 | :ibrowse.start() 10 | HttpServer.start(path: "/server", port: @port, response: "test_response") 11 | 12 | on_exit(fn -> 13 | HttpServer.stop(@port) 14 | end) 15 | 16 | :ok 17 | end 18 | 19 | test "print request/response" do 20 | assert capture_io(fn -> 21 | ExVCR.IEx.print do 22 | :ibrowse.send_req(~c"http://localhost:34005/server", [], :get) 23 | end 24 | end) =~ ~r/\"body\": \"test_response\"/ 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /test/ignore_localhost_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExVCR.IgnoreLocalhostTest do 2 | use ExVCR.Mock, adapter: ExVCR.Adapter.Hackney 3 | use ExUnit.Case, async: false 4 | 5 | @port 34012 6 | @url "http://localhost:#{@port}/server" 7 | 8 | setup_all do 9 | HTTPoison.start() 10 | 11 | on_exit(fn -> 12 | HttpServer.stop(@port) 13 | end) 14 | 15 | :ok 16 | end 17 | 18 | test "it does not record localhost requests when the config has been set" do 19 | use_cassette "ignore_localhost_on", ignore_localhost: true do 20 | HttpServer.start(path: "/server", port: @port, response: "test_response_before") 21 | assert HTTPoison.get!(@url, []).body =~ ~r/test_response_before/ 22 | HttpServer.stop(@port) 23 | # this method call should NOT be mocked 24 | HttpServer.start(path: "/server", port: @port, response: "test_response_after") 25 | assert HTTPoison.get!(@url, []).body =~ ~r/test_response_after/ 26 | HttpServer.stop(@port) 27 | end 28 | end 29 | 30 | test "it records localhost requests when the config has not been set" do 31 | use_cassette "ignore_localhost_unset" do 32 | HttpServer.start(path: "/server", port: @port, response: "test_response_before") 33 | assert HTTPoison.get!(@url, []).body =~ ~r/test_response_before/ 34 | HttpServer.stop(@port) 35 | # this method call should be mocked 36 | HttpServer.start(path: "/server", port: @port, response: "test_response_after") 37 | assert HTTPoison.get!(@url, []).body =~ ~r/test_response_before/ 38 | HttpServer.stop(@port) 39 | end 40 | end 41 | 42 | test "ignore_localhost option works with request headers" do 43 | use_cassette "ignore_localhost_with_headers", ignore_localhost: true do 44 | non_localhost_url = "http://127.0.0.1:#{@port}/server" 45 | HttpServer.start(path: "/server", port: @port, response: "test_response_before") 46 | assert HTTPoison.get!(non_localhost_url, [{"User-Agent", "ExVCR"}]).body =~ ~r/test_response_before/ 47 | HttpServer.stop(@port) 48 | # this method call should be mocked 49 | HttpServer.start(path: "/server", port: @port, response: "test_response_after") 50 | assert HTTPoison.get!(non_localhost_url, [{"User-Agent", "ExVCR"}]).body =~ ~r/test_response_before/ 51 | HttpServer.stop(@port) 52 | end 53 | end 54 | 55 | test "it records localhost requests when overrides the config setting" do 56 | ExVCR.Setting.set(:ignore_localhost, true) 57 | 58 | use_cassette "ignore_localhost_unset", ignore_localhost: false do 59 | HttpServer.start(path: "/server", port: @port, response: "test_response_before") 60 | assert HTTPoison.get!(@url, []).body =~ ~r/test_response_before/ 61 | HttpServer.stop(@port) 62 | # this method call should be mocked 63 | HttpServer.start(path: "/server", port: @port, response: "test_response_after") 64 | assert HTTPoison.get!(@url, []).body =~ ~r/test_response_before/ 65 | HttpServer.stop(@port) 66 | end 67 | 68 | ExVCR.Setting.set(:strict_mode, false) 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /test/ignore_urls_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExVCR.IgnoreUrlsTest do 2 | use ExVCR.Mock, adapter: ExVCR.Adapter.Hackney 3 | use ExUnit.Case, async: false 4 | 5 | @port 34013 6 | @url "http://localhost:#{@port}/server" 7 | @ignore_urls [ 8 | ~r/http:\/\/localhost.*/, 9 | ~r/http:\/\/127\.0\.0\.1.*/ 10 | ] 11 | 12 | setup_all do 13 | HTTPoison.start() 14 | 15 | on_exit(fn -> 16 | HttpServer.stop(@port) 17 | end) 18 | 19 | :ok 20 | end 21 | 22 | test "it does not record url requests when the config has been set" do 23 | use_cassette "ignore_urls_on", ignore_urls: @ignore_urls do 24 | HttpServer.start(path: "/server", port: @port, response: "test_response_before") 25 | assert HTTPoison.get!(@url, []).body =~ ~r/test_response_before/ 26 | HttpServer.stop(@port) 27 | # this method call should NOT be mocked 28 | HttpServer.start(path: "/server", port: @port, response: "test_response_after") 29 | assert HTTPoison.get!(@url, []).body =~ ~r/test_response_after/ 30 | HttpServer.stop(@port) 31 | # this method call should NOT be mocked 32 | non_localhost_url = "http://127.0.0.1:#{@port}/server" 33 | HttpServer.start(path: "/server", port: @port, response: "test_response_after") 34 | assert HTTPoison.get!(non_localhost_url, []).body =~ ~r/test_response_after/ 35 | HttpServer.stop(@port) 36 | end 37 | end 38 | 39 | test "it records urls requests when the config has not been set" do 40 | use_cassette "ignore_urls_unset" do 41 | HttpServer.start(path: "/server", port: @port, response: "test_response_before") 42 | assert HTTPoison.get!(@url, []).body =~ ~r/test_response_before/ 43 | HttpServer.stop(@port) 44 | # this method call should be mocked 45 | HttpServer.start(path: "/server", port: @port, response: "test_response_after") 46 | assert HTTPoison.get!(@url, []).body =~ ~r/test_response_before/ 47 | HttpServer.stop(@port) 48 | # this method call should NOT be mocked 49 | non_localhost_url = "http://127.0.0.1:#{@port}/server" 50 | HttpServer.start(path: "/server", port: @port, response: "test_response_after") 51 | assert HTTPoison.get!(non_localhost_url, []).body =~ ~r/test_response_before/ 52 | HttpServer.stop(@port) 53 | end 54 | end 55 | 56 | test "ignore_urls option works with request headers" do 57 | use_cassette "ignore_urls_with_headers", ignore_urls: @ignore_urls do 58 | HttpServer.start(path: "/server", port: @port, response: "test_response_after") 59 | assert HTTPoison.get!(@url, [{"User-Agent", "ExVCR"}]).body =~ ~r/test_response_after/ 60 | HttpServer.stop(@port) 61 | # this method call should be mocked 62 | non_localhost_url = "http://127.0.0.1:#{@port}/server" 63 | HttpServer.start(path: "/server", port: @port, response: "test_response_before") 64 | assert HTTPoison.get!(non_localhost_url, [{"User-Agent", "ExVCR"}]).body =~ ~r/test_response_before/ 65 | HttpServer.stop(@port) 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /test/mix/tasks_test.exs: -------------------------------------------------------------------------------- 1 | Code.require_file("../test_helper.exs", __DIR__) 2 | 3 | defmodule Mix.Tasks.VcrTest do 4 | use ExUnit.Case, async: true 5 | import ExUnit.CaptureIO 6 | 7 | @dummy_path "tmp/vcr_tmp/" 8 | @dummy_file1 "dummy1.json" 9 | @dummy_file2 "dummy2.json" 10 | @dummy_file_show "dummy_show.json" 11 | 12 | setup_all do 13 | File.mkdir_p!(@dummy_path) 14 | :ok 15 | end 16 | 17 | setup do 18 | ExVCR.Config.cassette_library_dir("fixture/vcr_cassettes", "fixture/custom_cassettes") 19 | end 20 | 21 | test "mix vcr" do 22 | assert capture_io(fn -> 23 | Mix.Tasks.Vcr.run([]) 24 | end) =~ ~r/Showing list of cassettes/ 25 | end 26 | 27 | test "mix vcr -h" do 28 | assert capture_io(fn -> 29 | Mix.Tasks.Vcr.run(["-h"]) 30 | end) =~ "Usage: mix vcr [options]" 31 | end 32 | 33 | test "mix vcr.delete" do 34 | File.touch!(@dummy_path <> @dummy_file1) 35 | 36 | assert capture_io(fn -> 37 | Mix.Tasks.Vcr.Delete.run(["--dir", @dummy_path, @dummy_file1]) 38 | end) =~ ~r/Deleted dummy1.json./ 39 | 40 | assert(File.exists?(@dummy_path <> @dummy_file1) == false) 41 | end 42 | 43 | test "mix vcr.delete with --interactive option" do 44 | File.touch!(@dummy_path <> @dummy_file1) 45 | 46 | assert capture_io("y\n", fn -> 47 | Mix.Tasks.Vcr.Delete.run(["-i", "--dir", @dummy_path, @dummy_file1]) 48 | end) =~ ~r/delete dummy1.json?/ 49 | 50 | assert(File.exists?(@dummy_path <> @dummy_file1) == false) 51 | end 52 | 53 | test "mix vcr.delete with --all option" do 54 | File.touch!(@dummy_path <> @dummy_file1) 55 | File.touch!(@dummy_path <> @dummy_file2) 56 | 57 | assert capture_io("y\n", fn -> 58 | Mix.Tasks.Vcr.Delete.run(["-a", "--dir", @dummy_path, @dummy_file1]) 59 | end) =~ ~r/Deleted dummy1.json./ 60 | 61 | assert(File.exists?(@dummy_path <> @dummy_file1) == false) 62 | assert(File.exists?(@dummy_path <> @dummy_file2) == false) 63 | end 64 | 65 | test "mix vcr.delete with invalid file" do 66 | assert capture_io(fn -> 67 | Mix.Tasks.Vcr.Delete.run(["--dir", @dummy_path]) 68 | end) =~ ~r/[Invalid Param]/ 69 | end 70 | 71 | test "mix vcr.show displays json content" do 72 | File.write(@dummy_path <> @dummy_file_show, "[{\"request\": \"a\"},{\"response\": {\"body\": \"dummy_body\"}}]") 73 | 74 | assert capture_io(fn -> 75 | Mix.Tasks.Vcr.Show.run([@dummy_path <> @dummy_file_show]) 76 | end) =~ ~r/dummy_body/ 77 | end 78 | 79 | test "mix vcr.show displays shows error if file is not found" do 80 | assert capture_io(fn -> 81 | Mix.Tasks.Vcr.Show.run(["invalid_file_name"]) 82 | end) =~ ~r/\[invalid_file_name\] was not found/ 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /test/mock_lock_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExVCR.MockLockTest do 2 | use ExUnit.Case, async: true 3 | 4 | test ":do_request_lock polls until lock is released" do 5 | caller_pid = self() 6 | test_pid = self() 7 | other_caller_pid = "fake_pid" 8 | state = %{lock_holder: other_caller_pid} 9 | 10 | {:noreply, _new_state} = 11 | ExVCR.MockLock.handle_info({:do_request_lock, caller_pid, test_pid}, state) 12 | 13 | assert_receive {:do_request_lock, ^caller_pid, ^test_pid} 14 | 15 | state2 = %{lock_holder: nil} 16 | 17 | {:noreply, new_state2} = 18 | ExVCR.MockLock.handle_info({:do_request_lock, caller_pid, test_pid}, state2) 19 | 20 | assert new_state2 == %{lock_holder: caller_pid} 21 | end 22 | 23 | test "removes lock when calling process goes down" do 24 | pid = "fake_pid" 25 | state = %{lock_holder: pid} 26 | 27 | {:noreply, new_state} = 28 | ExVCR.MockLock.handle_info({:DOWN, "ref", :process, pid, "reason"}, state) 29 | 30 | assert new_state == %{lock_holder: nil} 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /test/recorder_base_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExVCR.RecorderBaseTest do 2 | use ExUnit.Case, async: true 3 | use ExVCR.Mock 4 | alias ExVCR.Recorder 5 | 6 | test "initializes recorder" do 7 | record = Recorder.start(test: true, fixture: "fixture/tmp") 8 | assert ExVCR.Actor.Options.get(record.options) == [test: true, fixture: "fixture/tmp"] 9 | assert ExVCR.Actor.Responses.get(record.responses) == [] 10 | end 11 | 12 | test "test append/pop of recorder" do 13 | record = Recorder.start(test: true, fixture: "fixture/tmp") 14 | Recorder.append(record, "test") 15 | assert Recorder.pop(record) == "test" 16 | end 17 | 18 | test "return values from the block" do 19 | value = 20 | use_cassette "return_value_from_block" do 21 | 1 22 | end 23 | 24 | assert value == 1 25 | end 26 | 27 | test "return values from the block with stub mode" do 28 | value = 29 | use_cassette :stub, [] do 30 | 1 31 | end 32 | 33 | assert value == 1 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /test/recorder_httpc_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExVCR.RecorderHttpcTest do 2 | use ExUnit.Case, async: true 3 | use ExVCR.Mock, adapter: ExVCR.Adapter.Httpc 4 | 5 | @dummy_cassette_dir "tmp/vcr_tmp/vcr_cassettes_httpc" 6 | @port 34001 7 | @url ~c"http://localhost:#{@port}/server" 8 | @url_with_query ~c"http://localhost:#{@port}/server?password=sample" 9 | 10 | setup_all do 11 | on_exit(fn -> 12 | File.rm_rf(@dummy_cassette_dir) 13 | HttpServer.stop(@port) 14 | :ok 15 | end) 16 | 17 | Application.ensure_started(:inets) 18 | HttpServer.start(path: "/server", port: @port, response: "test_response") 19 | :ok 20 | end 21 | 22 | setup do 23 | ExVCR.Config.cassette_library_dir(@dummy_cassette_dir) 24 | end 25 | 26 | test "forcefully getting response from server by removing json in advance" do 27 | use_cassette "server1" do 28 | {:ok, {_, _, body}} = :httpc.request(@url) 29 | assert body =~ ~r/test_response/ 30 | end 31 | end 32 | 33 | test "forcefully getting response from server, then loading from cache by recording twice" do 34 | use_cassette "server2" do 35 | {:ok, {_, _, body}} = :httpc.request(@url) 36 | assert body =~ ~r/test_response/ 37 | end 38 | 39 | use_cassette "server2" do 40 | {:ok, {_, _, body}} = :httpc.request(@url) 41 | assert body =~ ~r/test_response/ 42 | end 43 | end 44 | 45 | test "replace sensitive data in body" do 46 | ExVCR.Config.filter_sensitive_data("test_response", "PLACEHOLDER") 47 | 48 | use_cassette "server_sensitive_data_in_body" do 49 | {:ok, {_, _, body}} = :httpc.request(@url) 50 | assert body =~ ~r/PLACEHOLDER/ 51 | end 52 | 53 | ExVCR.Config.filter_sensitive_data(nil) 54 | end 55 | 56 | test "replace sensitive data in query " do 57 | ExVCR.Config.filter_sensitive_data("password=[a-z]+", "password=***") 58 | 59 | use_cassette "server_sensitive_data_in_query" do 60 | {:ok, {_, _, body}} = :httpc.request(@url_with_query) 61 | assert body =~ ~r/test_response/ 62 | end 63 | 64 | # The recorded cassette should contain replaced data. 65 | cassette = File.read!("#{@dummy_cassette_dir}/server_sensitive_data_in_query.json") 66 | assert cassette =~ "password=***" 67 | refute cassette =~ "password=sample" 68 | 69 | ExVCR.Config.filter_sensitive_data(nil) 70 | end 71 | 72 | test "replace sensitive data in request header" do 73 | ExVCR.Config.filter_request_headers("X-My-Secret-Token") 74 | 75 | use_cassette "sensitive_data_in_request_header" do 76 | {:ok, {_, _, body}} = 77 | :httpc.request(:get, {@url_with_query, [{~c"X-My-Secret-Token", ~c"my-secret-token"}]}, [], []) 78 | 79 | assert body == "test_response" 80 | end 81 | 82 | # The recorded cassette should contain replaced data. 83 | cassette = File.read!("#{@dummy_cassette_dir}/sensitive_data_in_request_header.json") 84 | assert cassette =~ "\"X-My-Secret-Token\": \"***\"" 85 | refute cassette =~ "\"X-My-Secret-Token\": \"my-secret-token\"" 86 | 87 | ExVCR.Config.filter_request_headers(nil) 88 | end 89 | 90 | test "replace sensitive data in matching request header" do 91 | ExVCR.Config.filter_sensitive_data("Basic [a-z]+", "Basic ***") 92 | 93 | use_cassette "sensitive_data_matches_in_request_headers", match_requests_on: [:headers] do 94 | {:ok, {_, _, body}} = 95 | :httpc.request(:get, {@url_with_query, [{~c"Authorization", ~c"Basic credentials"}]}, [], []) 96 | 97 | assert body =~ ~r/test_response/ 98 | end 99 | 100 | # The recorded cassette should contain replaced data. 101 | cassette = File.read!("#{@dummy_cassette_dir}/sensitive_data_matches_in_request_headers.json") 102 | assert cassette =~ "\"Authorization\": \"Basic ***\"" 103 | 104 | # Attempt another request should match on filtered header 105 | use_cassette "sensitive_data_matches_in_request_headers", match_requests_on: [:headers] do 106 | {:ok, {_, _, body}} = 107 | :httpc.request(:get, {@url_with_query, [{~c"Authorization", ~c"Basic credentials"}]}, [], []) 108 | 109 | assert body =~ ~r/test_response/ 110 | end 111 | 112 | ExVCR.Config.filter_sensitive_data(nil) 113 | end 114 | 115 | test "filter url param flag removes url params when recording cassettes" do 116 | ExVCR.Config.filter_url_params(true) 117 | 118 | use_cassette "example_ignore_url_params" do 119 | {:ok, {_, _, body}} = :httpc.request(~c"#{@url}?should_not_be_contained") 120 | assert body =~ ~r/test_response/ 121 | end 122 | 123 | json = File.read!("#{__DIR__}/../#{@dummy_cassette_dir}/example_ignore_url_params.json") 124 | refute String.contains?(json, "should_not_be_contained") 125 | ExVCR.Config.filter_url_params(false) 126 | end 127 | 128 | test "remove blacklisted headers" do 129 | ExVCR.Config.response_headers_blacklist(["Date"]) 130 | 131 | use_cassette "remove_blacklisted_headers" do 132 | {:ok, {_, headers, _}} = :httpc.request(@url) 133 | assert Enum.sort(headers) == Enum.sort([{~c"server", ~c"Cowboy"}, {~c"content-length", ~c"13"}]) 134 | end 135 | 136 | ExVCR.Config.response_headers_blacklist([]) 137 | end 138 | end 139 | -------------------------------------------------------------------------------- /test/setting_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExVCR.SettingTest do 2 | use ExUnit.Case, async: true 3 | 4 | setup_all do 5 | cassette_library_dir = ExVCR.Setting.get(:cassette_library_dir) 6 | custom_library_dir = ExVCR.Setting.get(:custom_library_dir) 7 | 8 | on_exit(fn -> 9 | ExVCR.Setting.set(:cassette_library_dir, cassette_library_dir) 10 | ExVCR.Setting.set(:custom_library_dir, custom_library_dir) 11 | :ok 12 | end) 13 | 14 | :ok 15 | end 16 | 17 | test "set custom_library_dir" do 18 | ExVCR.Setting.set(:custom_library_dir, "custom_dummy") 19 | assert ExVCR.Setting.get(:custom_library_dir) == "custom_dummy" 20 | end 21 | 22 | test "set cassette_library_dir" do 23 | ExVCR.Setting.set(:cassette_library_dir, "cassette_dummy") 24 | assert ExVCR.Setting.get(:cassette_library_dir) == "cassette_dummy" 25 | end 26 | 27 | test "set response_headers_blacklist" do 28 | ExVCR.Setting.set(:response_headers_blacklist, ["Content-Type", "Accept"]) 29 | assert ExVCR.Setting.get(:response_headers_blacklist) == ["Content-Type", "Accept"] 30 | end 31 | 32 | test "set ignore_urls" do 33 | ExVCR.Setting.set(:ignore_urls, ["example.com"]) 34 | assert ExVCR.Setting.get(:ignore_urls) == ["example.com"] 35 | end 36 | 37 | test "append ignore_urls when there are no existing values" do 38 | ExVCR.Setting.append(:ignore_urls, "example.com") 39 | assert ExVCR.Setting.get(:ignore_urls) == ["example.com"] 40 | end 41 | 42 | test "append ignore_urls when there are existing values" do 43 | ExVCR.Setting.set(:ignore_urls, [~r/example.com/]) 44 | ExVCR.Setting.append(:ignore_urls, ~r/example2.com/) 45 | assert ExVCR.Setting.get(:ignore_urls) == [~r/example2.com/, ~r/example.com/] 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /test/strict_mode_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExVCR.StrictModeTest do 2 | use ExUnit.Case, async: false 3 | use ExVCR.Mock, adapter: ExVCR.Adapter.Hackney 4 | 5 | @dummy_cassette_dir "tmp/vcr_tmp/vcr_cassettes_strict_mode" 6 | @port 34007 7 | @url "http://localhost:#{@port}/server" 8 | @http_server_opts [path: "/server", port: @port, response: "test_response"] 9 | 10 | setup_all do 11 | File.rm_rf(@dummy_cassette_dir) 12 | 13 | on_exit(fn -> 14 | File.rm_rf(@dummy_cassette_dir) 15 | HttpServer.stop(@port) 16 | :ok 17 | end) 18 | 19 | HTTPoison.start() 20 | HttpServer.start(@http_server_opts) 21 | :ok 22 | end 23 | 24 | setup do 25 | ExVCR.Config.cassette_library_dir(@dummy_cassette_dir) 26 | end 27 | 28 | test "it makes HTTP calls if not set" do 29 | use_cassette "strict_mode_off", strict_mode: false do 30 | assert HTTPoison.get!(@url, []).body =~ ~r/test_response/ 31 | end 32 | end 33 | 34 | test "it throws an error when set and no cassette recorded" do 35 | use_cassette "strict_mode_on", strict_mode: true do 36 | try do 37 | HTTPoison.get!(@url, []).body =~ ~r/test_response/ 38 | assert(false, "Shouldn't get here") 39 | catch 40 | "A matching cassette was not found" <> _ -> :ok 41 | _ -> assert(false, "Encountered unexpected `throw`") 42 | end 43 | end 44 | end 45 | 46 | test "it uses a cassette if it exists" do 47 | use_cassette "strict_mode_cassette", strict_mode: false do 48 | assert HTTPoison.get!(@url, []).body =~ ~r/test_response/ 49 | end 50 | 51 | use_cassette "strict_mode_cassette", strict_mode: true do 52 | assert HTTPoison.get!(@url, []).body =~ ~r/test_response/ 53 | end 54 | end 55 | 56 | test "it does not uses a cassette when override the default config" do 57 | ExVCR.Setting.set(:strict_mode, true) 58 | 59 | use_cassette "strict_mode_cassette", strict_mode: false do 60 | assert HTTPoison.get!(@url, []).body =~ ~r/test_response/ 61 | end 62 | 63 | use_cassette "strict_mode_cassette" do 64 | assert HTTPoison.get!(@url, []).body =~ ~r/test_response/ 65 | end 66 | 67 | use_cassette "strict_mode_cassette", strict_mode: true do 68 | assert HTTPoison.get!(@url, []).body =~ ~r/test_response/ 69 | end 70 | 71 | ExVCR.Setting.set(:strict_mode, false) 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /test/task_runner_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExVCR.TaskRunnerTest do 2 | use ExUnit.Case, async: true 3 | import ExUnit.CaptureIO 4 | 5 | @deletes_path "test/cassettes/for_deletes/" 6 | 7 | test "show vcr cassettes task prints json file summary" do 8 | result = 9 | capture_io(fn -> 10 | ExVCR.Task.Runner.show_vcr_cassettes(["test/cassettes"]) 11 | end) 12 | 13 | assert result =~ ~r/[File Name]/ 14 | assert result =~ ~r/test1.json/ 15 | assert result =~ ~r/test2.json/ 16 | end 17 | 18 | test "delete cassettes task deletes json files" do 19 | File.mkdir_p!(@deletes_path) 20 | File.touch(@deletes_path <> "test1.json") 21 | File.touch(@deletes_path <> "test2.json") 22 | 23 | assert capture_io(fn -> 24 | ExVCR.Task.Runner.delete_cassettes(@deletes_path, "test1") 25 | end) == "Deleted test1.json.\n" 26 | 27 | File.rm(@deletes_path <> "test1.json") 28 | File.rm(@deletes_path <> "test2.json") 29 | end 30 | 31 | test "check vcr cassettes task prints json file summary" do 32 | result = 33 | capture_io(fn -> 34 | record = %ExVCR.Checker.Results{ 35 | dirs: ["test/cassettes"], 36 | files: [{:cache, "test1.json"}, {:cache, "test2.json"}, {:server, "test1.json"}, {:server, "test1.json"}] 37 | } 38 | 39 | ExVCR.Task.Runner.check_cassettes(record) 40 | end) 41 | 42 | assert result =~ ~r/Showing hit counts of cassettes in/ 43 | assert result =~ ~r/test1.json\s+1\s+2\s+\n/ 44 | assert result =~ ~r/test2.json\s+1\s+0\s+\n/ 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /test/task_util_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExVCR.TaskUtilTest do 2 | use ExUnit.Case, async: true 3 | 4 | test "default option" do 5 | option = ExVCR.Task.Util.parse_basic_options([]) 6 | assert option == ["fixture/vcr_cassettes", "fixture/custom_cassettes"] 7 | end 8 | 9 | test "custom option" do 10 | option = ExVCR.Task.Util.parse_basic_options(dir: "test1", custom: "test2") 11 | assert option == ["test1", "test2"] 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | Application.ensure_all_started(:http_server) 3 | Application.ensure_all_started(:telemetry) 4 | Finch.start_link(name: ExVCRFinch) 5 | --------------------------------------------------------------------------------