├── .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 |
--------------------------------------------------------------------------------