Code Monkey home page Code Monkey logo

exvcr's Introduction

ExVCR

Build Status Coverage Status hex.pm version hex.pm downloads License

Record and replay HTTP interactions library for Elixir. It's inspired by Ruby's VCR, and trying to provide similar functionalities.

Basics

The following HTTP libraries can be applied.

  • ibrowse{:target="_blank" rel="noopener"}-based libraries.
  • hackney{:target="_blank" rel="noopener"}-based libraries.
    • HTTPoison{:target="_blank" rel="noopener"}
    • support is very limited, and tested only with sync request of HTTPoison yet.
  • httpc{:target="_blank" rel="noopener"}-based libraries.
    • erlang-oauth{:target="_blank" rel="noopener"}
    • tirexs{:target="_blank" rel="noopener"}
    • support is very limited, and tested only with :httpc.request/1 and :httpc.request/4.
  • Finch{:target="_blank" rel="noopener"}
    • the deprecated Finch.request/6 functions is not supported

HTTP interactions are recorded as JSON file. The JSON file can be recorded automatically (vcr_cassettes) or manually updated (custom_cassettes).

Notes

  • ExVCR.Config functions must be called from setup or test. Calls outside of test process, such as in setup_all will not work.

Install

Add :exvcr to deps section of mix.exs.

def deps do
  [ {:exvcr, "~> 0.11", only: :test} ]
end

Optionally, preferred_cli_env: [vcr: :test] can be specified for running mix vcr in :test env by default.

def project do
  [ ...
    preferred_cli_env: [
      vcr: :test, "vcr.delete": :test, "vcr.check": :test, "vcr.show": :test
    ],
    ...
end

Usage

Add use ExVCR.Mock to the test module. This mocks ibrowse by default. For using hackney, specify adapter: ExVCR.Adapter.Hackney options as follows.

Example with ibrowse
defmodule ExVCR.Adapter.IBrowseTest do
  use ExUnit.Case, async: true
  use ExVCR.Mock

  setup do
    ExVCR.Config.cassette_library_dir("fixture/vcr_cassettes")
    :ok
  end

  test "example single request" do
    use_cassette "example_ibrowse" do
      :ibrowse.start
      {:ok, status_code, _headers, body} = :ibrowse.send_req('http://example.com', [], :get)
      assert status_code == '200'
      assert to_string(body) =~ ~r/Example Domain/
    end
  end

  test "httpotion" do
    use_cassette "example_httpotion" do
      HTTPotion.start
      assert HTTPotion.get("http://example.com", []).body =~ ~r/Example Domain/
    end
  end
end
Example with hackney
defmodule ExVCR.Adapter.HackneyTest do
  use ExUnit.Case, async: true
  use ExVCR.Mock, adapter: ExVCR.Adapter.Hackney

  setup_all do
    HTTPoison.start
    :ok
  end

  test "get request" do
    use_cassette "httpoison_get" do
      assert HTTPoison.get!("http://example.com").body =~ ~r/Example Domain/
    end
  end
end
Example with httpc
defmodule ExVCR.Adapter.HttpcTest do
  use ExUnit.Case, async: true
  use ExVCR.Mock, adapter: ExVCR.Adapter.Httpc

  setup_all do
    :inets.start
    :ok
  end

  test "get request" do
    use_cassette "example_httpc_request" do
      {:ok, result} = :httpc.request('http://example.com')
      {{_http_version, _status_code = 200, _reason_phrase}, _headers, body} = result
      assert to_string(body) =~ ~r/Example Domain/
    end
  end
end
Example with Finch
defmodule ExVCR.Adapter.FinchTest do
  use ExUnit.Case, async: true
  use ExVCR.Mock, adapter: ExVCR.Adapter.Finch

  setup_all do
    Finch.start_link(name: MyFinch)
    :ok
  end

  test "get request" do
    use_cassette "example_finch_request" do
      {:ok, response} = Finch.build(:get, "http://example.com/") |> Finch.request(MyFinch)
      assert response.status == 200
      assert Map.new(response.headers)["content-type"] == "text/html; charset=UTF-8"
      assert response.body =~ ~r/Example Domain/
    end
  end
end

Example with Start / Stop

Instead of single use_cassette, start_cassette and stop_cassette can serve as an alternative syntax.

use_cassette("x") do
  do_something
end
start_cassette("x")
do_something
stop_cassette

Custom Cassettes

You can manually define custom cassette JSON file for more flexible response control rather than just recoding the actual server response.

  • Optional 2nd parameter of ExVCR.Config.cassette_library_dir method specifies the custom cassette directory. The directory is separated from vcr cassette one for avoiding mistakenly overwriting.

  • Adding custom: true option to use_cassette macro indicates to use the custom cassette, and it just returns the pre-defined JSON response, instead of requesting to server.

defmodule ExVCR.MockTest do
  use ExUnit.Case, async: true
  import ExVCR.Mock

  setup do
    ExVCR.Config.cassette_library_dir("fixture/vcr_cassettes", "fixture/custom_cassettes")
    :ok
  end

  test "custom with valid response" do
    use_cassette "response_mocking", custom: true do
      assert HTTPotion.get("http://example.com", []).body =~ ~r/Custom Response/
    end
  end

The custom JSON file format is the same as vcr cassettes.

fixture/custom_cassettes/response_mocking.json

[
  {
    "request": {
      "url": "http://example.com"
    },
    "response": {
      "status_code": 200,
      "headers": {
        "Content-Type": "text/html"
      },
      "body": "<h1>Custom Response</h1>"
    }
  }
]

Recording VCR Cassettes

Matching

ExVCR uses URL parameter to match request and cassettes. The url parameter in the JSON file is taken as regexp string.

Removing Sensitive Data

ExVCR.Config.filter_sensitive_data(pattern, placeholder) method can be used to remove sensitive data. It searches for string matches with pattern, which is a string representing a regular expression, and replaces with placeholder. Replacements happen both in URLs and request and response bodies.

test "replace sensitive data" do
  ExVCR.Config.filter_sensitive_data("<PASSWORD>.+</PASSWORD>", "PLACEHOLDER")
  use_cassette "sensitive_data" do
    assert HTTPotion.get("http://something.example.com", []).body =~ ~r/PLACEHOLDER/
  end
end

ExVCR.Config.filter_request_headers(header) and ExVCR.Config.filter_request_options(option) can be used to remove sensitive data in the request headers. It checks if the header is found in the request headers and blanks out it's value with ***.

test "replace sensitive data in request header" do
  ExVCR.Config.filter_request_headers("X-My-Secret-Token")
  use_cassette "sensitive_data_in_request_header" do
    body = HTTPoison.get!("http://localhost:34000/server?", ["X-My-Secret-Token": "my-secret-token"]).body
    assert body == "test_response"
  end

  # The recorded cassette should contain replaced data.
  cassette = File.read!("#{@dummy_cassette_dir}/sensitive_data_in_request_header.json")
  assert cassette =~ "\"X-My-Secret-Token\": \"***\""
  refute cassette =~  "\"X-My-Secret-Token\": \"my-secret-token\""

  ExVCR.Config.filter_request_headers(nil)
end
test "replace sensitive data in request options" do
  ExVCR.Config.filter_request_options("basic_auth")
  use_cassette "sensitive_data_in_request_options" do
    body = HTTPoison.get!(@url, [], [hackney: [basic_auth: {"username", "password"}]]).body
    assert body == "test_response"
  end

  # The recorded cassette should contain replaced data.
  cassette = File.read!("#{@dummy_cassette_dir}/sensitive_data_in_request_options.json")
  assert cassette =~ "\"basic_auth\": \"***\""
  refute cassette =~  "\"basic_auth\": {\"username\", \"password\"}"

  ExVCR.Config.filter_request_options(nil)
end

Allowed hosts

The :ignore_urls can be used to allow requests to be made to certain hosts.

setup do
  ExVCR.Setting.set(:ignore_urls, [~/example.com/])
  ExVCR.Setting.append(:ignore_urls, ~/anotherurl.com/)
end

test "an actual request is made to example.com" do
  HTTPoison.get!("https://example.com/path?query=true")
  HTTPoison.get!("https://anotherurl.com/path?query=true")
end

Ignoring query params in URL

If ExVCR.Config.filter_url_params(true) is specified, query params in URL will be ignored when recording cassettes.

test "filter url param flag removes url params when recording cassettes" do
  ExVCR.Config.filter_url_params(true)
  use_cassette "example_ignore_url_params" do
    assert HTTPotion.get(
      "http://localhost:34000/server?should_not_be_contained", []).body =~ ~r/test_response/
  end
  json = File.read!("#{__DIR__}/../#{@dummy_cassette_dir}/example_ignore_url_params.json")
  refute String.contains?(json, "should_not_be_contained")

Removing headers from response

If ExVCR.Config.response_headers_blacklist(headers_blacklist) is specified, the headers in the list will be removed from the response.

test "remove blacklisted headers" do
  use_cassette "original_headers" do
    assert Map.has_key?(HTTPoison.get!(@url, []).headers, "connection") == true
  end

  ExVCR.Config.response_headers_blacklist(["Connection"])
  use_cassette "remove_blacklisted_headers" do
    assert Map.has_key?(HTTPoison.get!(@url, []).headers, "connection") == false
  end

  ExVCR.Config.response_headers_blacklist([])
end

Matching Options

Matching against query params

By default, query params are not used for matching. In order to include query params, specify match_requests_on: [:query] for use_cassette call.

test "matching query params with match_requests_on params" do
  use_cassette "different_query_params", match_requests_on: [:query] do
    assert HTTPotion.get("http://localhost/server?p=3", []).body =~ ~r/test_response3/
    assert HTTPotion.get("http://localhost/server?p=4", []).body =~ ~r/test_response4/
  end
end
Matching against request body

By default, request body is not used for matching. In order to include request body, specify match_requests_on: [:request_body] for use_cassette call.

test "matching request body with match_requests_on params" do
  use_cassette "different_request_body_params", match_requests_on: [:request_body] do
    assert HTTPotion.post("http://localhost/server", [body: "p=3"]).body =~ ~r/test_response3/
    assert HTTPotion.post("http://localhost/server", [body: "p=4"]).body =~ ~r/test_response4/
  end
end
Matching against custom parameters

You can define and use your own matchers for cases not covered by the build-in matchers. To do this you can specify custom_matchers: [func_one, func_two, ...] for use_cassette call.

test "matching special header with custom_matchers" do
  matches_special_header = fn response, keys, _recorder_options ->
    recorded_headers = always_map(response.request.headers)
    expected_value = recorded_headers["X-Special-Header"]
    keys[:headers]
    |> Enum.any?(&(match?({"X-Special-Header", ^expected_value}, &1)))
  end

  use_cassette "special_header_match", custom_matchers: [matches_special_header] do
    # These two requests will match with each other since our custom matcher matches (even if without matching all headers)
    assert HTTPotion.post("http://localhost/server",
        [headers: ["User-Agent": "My App", "X-Special-Header": "Value One"]]).body =~ ~r/test_response_one/
    assert HTTPotion.post("http://localhost/server",
        [headers: ["User-Agent": "Other App", "X-Special-Header": "Value One"]]).body =~ ~r/test_response_one/

    # This will not match since the header has a different value:
    assert HTTPotion.post("http://localhost/server",
        [headers: ["User-Agent": "My App", "X-Special-Header": "Value Two"]]).body =~ ~r/test_response_two/
  end
end

Default Config

Default parameters for ExVCR.Config module can be specified in config\config.exs as follows.

use Mix.Config

config :exvcr, [
  vcr_cassette_library_dir: "fixture/vcr_cassettes",
  custom_cassette_library_dir: "fixture/custom_cassettes",
  filter_sensitive_data: [
    [pattern: "<PASSWORD>.+</PASSWORD>", placeholder: "PASSWORD_PLACEHOLDER"]
  ],
  filter_url_params: false,
  filter_request_headers: [],
  response_headers_blacklist: []
]

If exvcr is defined as test-only dependency, describe the above statement in test-only config file (ex. config\test.exs) or make it conditional (ex. wrap with if Mix.env == :test).

Global mock experimental feature

The global mock is an attempt to address a general issue with exvcr being slow, see #107

In general, every use_cassette takes around 500 ms so if you extensively use cassettes it could spend minutes doing :meck.expect/2 and :meck.unload/1. Even exvcr tests need 40 seconds versus 1 second when global mock is used.

Since feature is experimental be careful when using it. Please note the following:

  • ExVCR implements global mock, which means that all HTTP client calls outside of use_cassette go through meck.passthough/1.
  • There are some report that the feature doesn't work in some case, see the issue.
  • By default, the global mocking disabled, to enabled it set the following in config:
use Mix.Config

config :exvcr, [
  global_mock: true
]

All tests that are written for exvcr could also be running in global mocking mode:

$ GLOBAL_MOCK=true mix test

.........................................................

Finished in 1.3 seconds
141 tests, 0 failures

Randomized with seed 905427

Mix Tasks

The following tasks are added by including exvcr package.

[mix vcr] Show cassettes

$ mix vcr
Showing list of cassettes in [fixture/vcr_cassettes]
  [File Name]                              [Last Update]
  example_httpotion.json                   2013/11/07 23:24:49
  example_ibrowse.json                     2013/11/07 23:24:49
  example_ibrowse_multiple.json            2013/11/07 23:24:48
  httpotion_delete.json                    2013/11/07 23:24:47
  httpotion_patch.json                     2013/11/07 23:24:50
  httpotion_post.json                      2013/11/07 23:24:51
  httpotion_put.json                       2013/11/07 23:24:52

Showing list of cassettes in [fixture/custom_cassettes]
  [File Name]                              [Last Update]
  method_mocking.json                      2013/10/06 22:05:38
  response_mocking.json                    2013/09/29 17:23:38
  response_mocking_regex.json              2013/10/06 18:13:45

[mix vcr.delete] Delete cassettes

The mix vcr.delete task deletes the cassettes that contains the specified pattern in the file name.

$ mix vcr.delete ibrowse
Deleted example_ibrowse.json.
Deleted example_ibrowse_multiple.json.

If -i (--interactive) option is specified, it asks for confirmation before deleting each file.

$ mix vcr.delete ibrowse -i
delete example_ibrowse.json? y
Deleted example_ibrowse.json.
delete example_ibrowse_multiple.json? y
Deleted example_ibrowse_multiple.json.

If -a (--all) option is specified, all the cassettes in the specified folder becomes the target for delete.

[mix vcr.check] Check cassettes

The mix vcr.check shows how many times each cassette is applied while executing mix test tasks. It is intended for verifying the cassettes are properly used. [Cassette Counts] indicates the count that the pre-recorded JSON cassettes are applied. [Server Counts] indicates the count that server access is performed.

$ mix vcr.check
...............................
31 tests, 0 failures
Showing hit counts of cassettes in [fixture/vcr_cassettes]
  [File Name]                              [Cassette Counts]    [Server Counts]
  example_httpotion.json                   1                    0
  example_ibrowse.json                     1                    0
  example_ibrowse_multiple.json            2                    0
  httpotion_delete.json                    1                    0
  httpotion_patch.json                     1                    0
  httpotion_post.json                      1                    0
  httpotion_put.json                       1                    0
  sensitive_data.json                      0                    2
  server1.json                             0                    2
  server2.json                             2                    2

Showing hit counts of cassettes in [fixture/custom_cassettes]
  [File Name]                              [Cassette Counts]    [Server Counts]
  method_mocking.json                      1                    0
  response_mocking.json                    1                    0
  response_mocking_regex.json              1                    0

The target test file can be limited by specifying test files, as similar as mix test tasks.

$ mix vcr.check test/exvcr_test.exs
.............
13 tests, 0 failures
Showing hit counts of cassettes in [fixture/vcr_cassettes]
  [File Name]                              [Cassette Counts]    [Server Counts]
  example_httpotion.json                   1                    0
...
...

[mix vcr.show] Show cassettes

The mix vcr.show task displays the contents of cassettes JSON file in the prettified format.

$ mix vcr.show fixture/vcr_cassettes/httpoison_get.json
[
  {
    "request": {
      "url": "http://example.com",
      "headers": [],
      "method": "get",
      "body": "",
      "options": []
    },
...

[mix vcr --help] Help

Displays helps for mix sub-tasks.

$ mix vcr --help
Usage: mix vcr [options]
  Used to display the list of cassettes

  -h (--help)         Show helps for vcr mix tasks
  -d (--dir)          Specify vcr cassettes directory
  -c (--custom)       Specify custom cassettes directory

Usage: mix vcr.delete [options] [cassette-file-names]
  Used to delete cassettes

  -d (--dir)          Specify vcr cassettes directory
  -c (--custom)       Specify custom cassettes directory
  -i (--interactive)  Request confirmation before attempting to delete
  -a (--all)          Delete all the files by ignoring specified [filenames]

Usage: mix vcr.check [options] [test-files]
  Used to check cassette use on test execution

  -d (--dir)          Specify vcr cassettes directory
  -c (--custom)       Specify custom cassettes directory

Usage: mix vcr.show [cassette-file-names]
  Used to show cassette contents
Notes

If the cassette save directory is changed from the default, [-d, --dir] option (for vcr cassettes) and [-c, --custom] option (for custom cassettes) can be used to specify the directory.

IEx Helper

ExVCR.IEx module provides simple helper functions to display the HTTP request/response in JSON format, instead of recording in the cassette files.

% iex -S mix
Erlang R16B03 (erts-5.10.4) ...
Interactive Elixir (0.12.5) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> require ExVCR.IEx
nil
iex(2)> ExVCR.IEx.print do
...(2)>   :ibrowse.send_req('http://example.com', [], :get)
...(2)> end
[
  {
    "request": {
      "url": "http://example.com",
      "headers": [],
      "method": "get",
      "body": "",
      "options": []
    },
    "response": {
      "type": "ok",
      "status_code": 200,
...

The adapter option can be specified as adapter argument of print function, as follows.

% iex -S mix
Erlang R16B03 (erts-5.10.4) ...

Interactive Elixir (0.12.5) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> require ExVCR.IEx
nil
iex(2)> ExVCR.IEx.print(adapter: ExVCR.Adapter.Hackney) do
...(2)>   HTTPoison.get!("http://example.com").body
...(2)> end
[
  {
    "request": {
      "url": "http://example.com",
...

Stubbing Response

Specifying :stub as fixture name allows directly stubbing the response header/body information based on parameter.

test "stub request works for HTTPotion" do
  use_cassette :stub, [url: "http://example.com", body: "Stub Response", status_code: 200] do
    response = HTTPotion.get("http://example.com", [])
    assert response.body =~ ~r/Stub Response/
    assert response.headers[:"Content-Type"] == "text/html"
    assert response.status_code == 200
  end
end

test "stub request works for HTTPoison" do
  use_cassette :stub, [url: "http://www.example.com", body: "Stub Response"] do
    response = HTTPoison.get!("http://www.example.com")
    assert response.body =~ ~r/Stub Response/
    assert response.headers["Content-Type"] == "text/html"
    assert response.status_code == 200
  end
end

test "stub request works for httpc" do
  use_cassette :stub, [url: "http://www.example.com",
                       method: "get",
                       status_code: ["HTTP/1.1", 200, "OK"],
                       body: "success!"] do

  {:ok, result} = :httpc.request('http://example.com')
  {{_http_version, _status_code = 200, _reason_phrase}, _headers, body} = result
  assert to_string(body) == "success!"
end

test "stub request works for Finch" do
  use_cassette :stub, [url: "http://www.example.com",
                       method: "get",
                       status_code: 200,
                       body: "Stub Response"] do

  {:ok, response} = Finch.build(:get, "http://example.com/") |> Finch.request(MyFinch)
  assert response.body =~ ~r/Stub Response/
  assert Map.new(response.headers)["content-type"] == "text/html"
  assert response.status_code == 200
end

test "stub multiple requests works on Finch" do
  stubs = [
    [url: "http://example.com/1", body: "Stub Response 1", status_code: 200],
    [url: "http://example.com/2", body: "Stub Response 2", status_code: 404]
  ]

  use_cassette :stub, stubs do
    {:ok, response} = Finch.build(:get, "http://example.com/1") |> Finch.request(ExVCRFinch)
    assert response.status == 200
    assert response.body =~ ~r/Stub Response 1/

    {:ok, response} = Finch.build(:get, "http://example.com/2") |> Finch.request(ExVCRFinch)
    assert response.status == 404
    assert response.body =~ ~r/Stub Response 2/
  end
end

If the specified :url parameter doesn't match requests called inside the use_cassette block, it raises ExVCR.InvalidRequestError.

The :url can be regular expression string. Please note that you should use the ~r sigil with / as delimiters.

test "match URL with regular expression" do
  use_cassette :stub, [url: "~r/(foo|bar)/", body: "Stub Response", status_code: 200] do
    # ...
  end
end

test "make sure to properly escape the /" do
  use_cassette :stub, [url: "~r/\/path\/to\/file\/without\/trailing\/slash\/does\/not\/work", body: "Stub Response", status_code: 200] do
    # ...
  end
end

test "the sigil delimiter cannot be anything else" do
  use_cassette :stub, [url: "~r{this-delimiter-does-not-work}", body: "Stub Response", status_code: 200] do
    # ...
  end
end

TODO

  • Improve performance, as it's very slow.

exvcr's People

Contributors

blakewilliams avatar boone avatar ciaran avatar clekstro avatar drapergeek avatar ecarnevale avatar edgurgel avatar enilsen16 avatar frekw avatar frm avatar gmile avatar gullitmiranda avatar javierjulio avatar joe-noh avatar kianmeng avatar marcandre avatar myronmarston avatar nemo157 avatar nurugger07 avatar optikfluffel avatar parroty avatar reisub avatar schnittchen avatar smeevil avatar surik avatar tt avatar vanstee avatar wojtekmach avatar zhyu avatar zoldar avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

exvcr's Issues

Add ignore url/host to config

Hi,

We're using ExVCR and we're very happy with it but sometimes we have an issue with some requests that we don't want to be intercepted.

We've looked at the README thoroughly but haven't find any clue about how to do this. We've also looked at the doc and still no clue...

Would you be interested in adding such a functionnality to ExVCR?

I guess something like an attribute that we can add in the config like an array of strings or regexes:

config :exvcr, [
  # ...
  ignore_urls: ["https://www.iana.org/time-zones/repository/tzdata-latest.tar.gz", ~r(^https://\w+.iana.org/.*)]
]

We can of course do a pull request if you're ok with it, we just want to check before we invest some hours of dev. 😉

Cheers!

Interaction with Hound

I'm building a web scraper using Hound, and it works fine, but when trying to test it using exvcr to mock the responses the responses are pretty much empty (at least no interesting data). I'm not sure if this is an exvcr or hound issue, or even if it is an issue with the way I have them set up, but hopefully you can help me.

Here's the code for the files I think are relevant. On the cassette, the important request is on line 68.

Bypass recorded responses

We like VCR, and would like to use it in our test suite. We have a suite, that is full of use_cassette calls

Most of the time we are happy with out test suite hitting recorded response. However, right before deploy, we'd like our requests to hit a live remote service. Unfortunately, it means that we need to remove a folder with all cassettes every time we actually need to hit external service.

What we'd like to have is a configurable ability to bypass cassettes.

Options to implement this include:

  1. make use_cassette include a flag, say skip: true, to skip or not skip the cassette. We then could make something like:

    use_cassette "path/to/my_cassette", skip: System.get_env(:RUN_LIVE_REQUESTS) do
    # ...
    end
  2. same as in item 1, only make this configurable at a higher level, e.g. in config :exvcr, skip_cassettes: true,

  3. have implemented both items 1 and 2, with setting for 1 overriding setting from 2.

no function clause matching in ExVCR.Adapter.Httpc.apply_filters/1

Hi, while trying to use exvcr with my pet project and ExTwitter I get this:

19:53:43.564 [error] Process #PID<0.197.0> raised an exception
** (FunctionClauseError) no function clause matching in ExVCR.Adapter.Httpc.apply_filters/1
    lib/exvcr/adapter/httpc.ex:56: ExVCR.Adapter.Httpc.apply_filters({:ok, #Reference<0.0.2.254>})
    lib/exvcr/handler.ex:136: ExVCR.Handler.get_response_from_server/2
    (inets) :httpc.request(:post, {'https://stream.twitter.com/1.1/statuses/filter.json', [], 'application/x-www-form-urlencoded', 'oauth_signature=...'}, [autoredirect: false], [sync: false, stream: :self])
    (extwitter) lib/extwitter/api/streaming.ex:65: anonymous fn/3 in ExTwitter.API.Streaming.spawn_async_request/1

What am I doing wrong? If you need more details please let me know, thanks!

Cassette is not ejected after the test

Right now this happens:

  1. I have a file with multiple test cases,
  2. each case is wrapped into with use_cassette/2 macro,
  3. bottom-most test case is called X,
  4. I add a new test case Y that performs HTTP request, but I forget to wrap it into a use_cassette/2 macro,
  5. I run the tests,
  6. test case Y fails with a message: "cassette from test case X doesn't contain a response matching a request from Y".

This can be seen in this PR: beam-community/stripity-stripe#80
Here's a failing travis build: https://travis-ci.org/robconery/stripity-stripe/builds/153164601

ibrowse example is not working

Hi I have tried to setup exvcr and use the example to ensure everything is working but I keep getting the following error:

** (ErlangError) erlang error: {:not_mocked, :ibrowse}
     stacktrace:
       src/meck_proc.erl:118: :meck_proc.set_expect/2
       src/meck.erl:234: :meck.expect/3
       (elixir) lib/enum.ex:651: Enum."-each/2-lists^foreach/1-0-"/2
       (elixir) lib/enum.ex:651: Enum.each/2
       test/lib/mail_chimp_test.exs:46: (test)

my code is as follows (the example provided by exvcr):

defmodule ExtraTurn.MailChimpTest do
  use ExUnit.Case, async: false
  use ExVCR.Mock

  setup_all do
    ExVCR.Config.cassette_library_dir("fixture/vcr_cassettes")
    :ok
  end

  test "example single request" do
    use_cassette "example_ibrowse" do
      :ibrowse.start
      {:ok, status_code, _headers, body} = :ibrowse.send_req('http://example.com', [], :get)
      assert status_code == '200'
      assert to_string(body) =~ ~r/Example Domain/
    end
  end

  test "httpotion" do
    use_cassette "example_httpotion" do
      HTTPotion.start
      assert HTTPotion.get("http://example.com", []).body =~ ~r/Example Domain/
    end
  end
end

I am using the latest elixir version, 1.3.4:

Erlang/OTP 19 [erts-8.1] [source] [64-bit] [smp:8:8] [async-threads:10] [hipe] [kernel-poll:false] [dtrace]

Elixir 1.3.4

and using Phoenix 1.2.1 (but I don't think that will affect anything).

I have tried using both the latest version on hex.pm and the latest github version.

Any help would be great!

Inconsistent behavior when using ExVCR to test logic that makes HTTP requests in spawned processes

We've been using ExVCR for a while and for the most part it's working well for us. However, I've spent a few hours using it in a certain situation where it's not working correctly at all and is producing inconsistent results. Here's our setup:

  • We have an umbrella app with multiple child applications including barbosa_client (a simple HTTP client for an internal service using httpoison) and api (a phoenix app).
  • ExVCR works just fine in the tests in barbosa_client
  • In api, we have a phoenix channel that:
    • Receives a get_data event
    • Spawns 4 linked processes that each get part of the data from a different backend data source (although only one of these uses HTTP: the one that uses barbosa_client. The others read data off the file system or from a SQL DB)
    • The 4 linked processes send the fetched data back to the phoenix channel process, which then sends the data over a socket back to the client

I've tried to use ExVCR in the test of the phoenix channel in api and it's not working right at all. Specifically, the results are inconsistent -- I ran it a bunch of times to get it to record the interaction and I got several different results. Eventually, I got it to record, but now it plays back inconsistently. Occasionally (less than 10%) of the time, but the rest of the time it gets one of a few different failures.

Here are the different failures I see (both when recording and when trying to play back). Most common is this:

  1) test sends the expected responses (and only the expected responses) when it receives params (DeloreanAPI.KeywordAnalysisChannelAcceptanceTest)
     apps/api/test/acceptance/keyword_analysis_channel_test.exs:27
     ** (EXIT from #PID<0.14215.0>) killed

Some process exited, but it provides no detail to understand what happen, unfortunately. I occasionally see this:

     ** (EXIT from #PID<0.1154.0>) an exception was raised:
         ** (RuntimeError) {:error, "BarbosaClient.difficulty_for(\"rspec before\", \"google.en-US\") post error (reason: %HTTPoison.Error{id: nil, reason: :req_not_found}); url: http://[REDACTED]/barbosa-internal-api/0.0.1/keyword/difficulty, request_body: %{engine: \"google\", keyword: \"rspec before\", locale: \"en-US\"}, headers: [{\"Content-type\", \"application/json\"}]"}
             (rankings_endpoint_models) lib/keyword_analysis/keyword_difficulty.ex:12: Delorean.RankingsEndpointModels.KeywordAnalysis.KeywordDifficulty.get/1
             (api) web/channels/keyword_analysis_channel.ex:120: anonymous fn/5 in DeloreanAPI.KeywordAnalysisChannel.announce_and_start_with_params/3

Once I get this head-scratcher:

         ** (ExVCR.RequestNotMatchError) Request did not match with any one in the current cassette: /Users/myron/moz/delorean/vcr_cassettes/recorded/link_opportunities_acceptance_test.json.
     Delete the current cassette with [mix vcr.delete] and re-record.

             lib/exvcr/handler.ex:127: ExVCR.Handler.raise_error_if_cassette_already_exists/1
             lib/exvcr/handler.ex:111: ExVCR.Handler.get_response_from_server/2
             (hackney) :hackney.request(:post, "http://staging.roger.dal.moz.com/barbosa-internal-api/0.0.1/keyword/difficulty", [{"Content-type", "application/json"}], "{\"locale\":\"en-US\",\"keyword\":\"rspec before\",\"engine\":\"google\"}", [])
             (httpoison) lib/httpoison/base.ex:396: HTTPoison.Base.request/9
             (rest_client) lib/rest_client.ex:33: anonymous fn/5 in Delorean.RestClient.post/5
             (stdlib) timer.erl:166: :timer.tc/1
             (util) lib/monitor.ex:42: Delorean.Util.Monitor.track/1
             (util) lib/monitor.ex:32: Delorean.Util.Monitor.perform_action_and_log/2
             (barbosa_client) lib/barbosa_client.ex:25: Delorean.BarbosaClient.difficulty_for/3
             (rankings_endpoint_models) lib/keyword_analysis/keyword_difficulty.ex:8: Delorean.RankingsEndpointModels.KeywordAnalysis.KeywordDifficulty.get/1
             (api) web/channels/keyword_analysis_channel.ex:120: anonymous fn/5 in DeloreanAPI.KeywordAnalysisChannel.announce_and_start_with_params/3

What's odd about this is that link_opportunities_acceptance_test.json is not used in this test -- it's used in a completely different test in a completely different file.

I've confirmed that we have async: false in all of our tests that use ExVCR so it can't be async tests at fault (also, adding --max-cases 1 to prevent any async tests resulted in the same inconsistent weirdness).

I've tried isolating this to a simple reproducible example I can provide for you but haven't yet gotten that (sorry), but I could perhaps spend more time on that if you really need it.

I've been assuming the parallel spawned processes in the channel are at fault but (a) only one of them makes an HTTP request so there are no parallel requests happening and (b) when I move the request into a spawned process in other tests where ExVCR is working it keep working just fine...so that may be a red herring.

I did a bit of looking around the ExVCR source and noticed some places where I'd expect there to be race conditions if ExVCR was used for a test of code that makes parallel requests. I'm not sure if these are related or not, but thought I'd mention them all the same:

  • ExVCR.Actor.Responses.pop/1: between the get and the set call the GenServer could respond to another message, right? I think this could be fixed by moving the logic behind a defcall so that it all happens within the GenServer, ensuring it happens synchronously.
  • ExVCR.Checker.add_count/2: Ditto here: the genserver state could change between get and append and I think this logic should happen within the GenServer instead of in the client process.
  • ExVCR.Recorder.update/3: There are multiple get gen sever calls in the map followed by a set at the end and again, the genserver state could change in the meantime. (Also, if the updater or finder functions use any GenServers they could lead to race conditions as well).

To prevent race conditions, I'd expect any operation that needs to be treated atomically to happen within a single GenServer.

Thanks!

ExVCR seems to play back responses just recorded during a recording session

I'm working on a test for an API that is stateful and does not always return the same response for the same request. The code I am testing makes the same request multiple times expecting different responses because the API behaves that way, but ExVCR is getting in the way. When the 2nd request happens, it appears to playback the response for the 1st request, even though the cassette does not even exist when I run my test--at least, my test is failing due to getting an identical response as was received on the first request, and the recorded cassette contains only one interaction.

To provide a more concrete example, think about an banking API that provides /current-balance and /send-payment endpoints. One can imagine writing a test that does this sequence:

  1. Calls /current-balance and gets the current balance.
  2. Calls /send-payment with n dollars.
  3. Calls /current-balance and asserts the new balance is original_balance - n.

Can ExVCR be fixed so that no playback happens when the cassette did not exist at the point the test began, no playback occurs? Given that APIs can be stateful it is often necessary to make the same request multiple times as part of a test, expecting different responses.

Argument Error for JSX

1) test fetch lead (ExCloseioTest)
     test/ex_closeio_test.exs:11
     ** (ArgumentError) argument error
     stacktrace:
       lib/jsx.ex:154: JSX.Encoder.Tuple.json/1
       lib/jsx.ex:121: JSX.Encoder.List.unzip/1
       lib/jsx.ex:114: JSX.Encoder.List.json/1
       lib/jsx.ex:105: JSX.Encoder.Map.unpack/2
       lib/jsx.ex:105: JSX.Encoder.Map.unpack/2
       lib/jsx.ex:101: JSX.Encoder.Map.json/1
       lib/jsx.ex:105: JSX.Encoder.Map.unpack/2
       lib/jsx.ex:101: JSX.Encoder.Map.json/1
       lib/jsx.ex:126: JSX.Encoder.List.unhitch/1
       lib/jsx.ex:117: JSX.Encoder.List.json/1
       lib/jsx.ex:4: JSX.encode!/2
       lib/exvcr/json.ex:10: ExVCR.JSON.save/2
       test/ex_closeio_test.exs:15

Finished in 2.9 seconds (0.2s on load, 2.7s on tests)
1 test, 1 failure

I'm not entirely sure what the problem is here. I'm new to Elixir so I haven't really figured out a good way to debug errors that occur in the dependency chain.

I'm use HTTPoison to make the requests, they seems to working fine and returning correct responses, it looks like the error is occuring when ExVCR tries to record something.

Ignored_hosts option

The original VCR has an ignore_hosts option, any requests made to those hosts will bypass VCR completely. Is there an equivalent option or workaround that could be used to achieve the same?

Doesn't mock requests......

defmodule AbrPayments.Api.V1.PaymentsControllerTest do
  use AbrPayments.ConnCase
  use ExUnit.Case, async: false
  use ExVCR.Mock, adapter: ExVCR.Adapter.Httpc

  setup_all do
    AbrPayments.Abr.Http.start
    ExVCR.Config.cassette_library_dir("fixture/vcr_cassettes", "fixture/custom_cassettes")
    :ok
  end

  test "returns 401 if don't send token" do
    conn = conn
            |> get(payments_path(conn, :create))
            |> doc
    assert conn.status == 401
  end

  test "return 422 if accounts in form not user account" do
    use_cassette :stub, [url: "http://localhost:3000/api/v1/accounts/for_payments", body: "Stub Response"] do
      {:ok, body} = HTTPoison.get!("http://localhost:3000/api/v1/accounts/for_payments", [])
      IO.inspect(body)
    end
  end
test "stub request works for HTTPoison" do
  use_cassette :stub, [url: "http://www.example.com", body: "Stub Response"] do
    response = HTTPoison.get!("http://www.example.com")
    assert response.body =~ ~r/Stub Response/
    assert response.headers["Content-Type"] == "text/html"
    assert response.status_code == 200
  end
end
end

Match requests on query params and request body by default

It seems to me that use_cassette should default to using the options match_requests_on: [:query, :request_body]. If you are submitting different data, it's likely that the server will respond differently. exvcr implicitly hides those different responses, which is both unexpected and likely to lead to false test coverage.

I'm suggesting that the options for match_requests_on: [:query, :request_body] should be opt-out instead of opt-in. At the very least, there should be a global config option to override this behavior, but I'd rather push people in the right direction by default.

I'm happy to write some code for this, but I wanted to get your response before starting anything.

Mock multiple identical requests with different responses

My app polls a URL which returns "pending" until it returns "done". The URL is exactly the same each time I poll. Is it possible for exvcr to record the fact that the first request and the last request should send different responses?

VCR creates downcased folder names

I use the following naming for my cassettes:

use_cassette "Stripe.PlanTest/count" do

The above means I want to keep count.json inside Stripe.PlanTest folder, which is a name for my test module.

However VCR down cases the name of a folder to: stripe.plantest. Can this be changed to not perform downcase on write?

Stubbing POST?

I've been using the use_cassette :stub, [status_code: 500] option for a while and it works great for GET requests. When I moved to using a POST however, the stub doesn't seem to be catching anything.

Things I've tried:

  • Specifying the URL
  • Specifying the post method as "POST" or "post"
  • Upgrading to the newest version of VCR.

I'm using the Hackney adapter and the request is going through HTTPoison. Any ideas would be really helpful. Thanks!

Licensing

Hi,

would it be possible to add an MIT or other license to this project?

Thanks!

Stub a new response from an existing HTTP dump/fixture

One of the features I'm missing (that I was using all the time in Ruby and in other languages) is the ability to load a response from an existing HTTP response fixture.

I wrote a super simple helper in a client I'm working on right now that allows you to use an existing fixture as a cassette:

use_cassette :stub, ExvcrUtils.response_fixture("auth/whoami/success.http", [url: "~r/\/v2\/whoami/$"]) do
  # ...
end

This was inspired by how Webmock behaves in Ruby. You can pass a full HTTP response as string to to_return, and it will be parsed.

      stub_request(:post, %r[/v2/#{account_id}/domains$])
          .to_return(" ... ")

rather than having to pass each response component separately (code, headers, body).

Would you be interested in having something similar built into ExVCR? If that's the case, I can provide a PR with the patch.

It could be directly integrated in prepare_stub_record: if the value of options[:response] exists and it is a string, then parse it. I would delegate to the caller the responsibility to load the response (from file, a variable, or somewhere else).

Does it make sense to you?

request_body doesn't match the body of the request when :stub

I'm having hard time trying to define a stub for a call that has a body payload.

You can see the tests here
https://github.com/weppos/dnsimple-elixir/blob/test-create/test/dnsimple_domains_service_test.exs#L32-L51

It looks like the matcher doesn't properly match the body. Take for example this code fragment

    fixture = ExvcrUtils.response_fixture("createDomain/created.http", [url: "~r/\/v2/$", request_body: ""])
    use_cassette :stub, fixture do
      { :ok, response } = @service.create_domain(@client, "1010", "")
      assert response.__struct__ == Dnsimple.Response

the error message is

  1) test .create_domains returns a Dnsimple.Response (DnsimpleDomainsServiceTest)
     test/dnsimple_domains_service_test.exs:39
     ** (ExVCR.InvalidRequestError) response for [URL:https://api.dnsimple.test/v2/1010/domains, METHOD:post] was not found
     stacktrace:
       lib/exvcr/handler.ex:28: ExVCR.Handler.get_response_from_cache/2
       lib/exvcr/handler.ex:13: ExVCR.Handler.get_response/2
       (hackney) :hackney.request(:post, "https://api.dnsimple.test/v2/1010/domains", [{"Accept", "application/json"}, {"User-Agent", "dnsimple-elixir/0.0.1"}, {"Authorization", "Bearer i-am-a-token"}], "", [])
       (httpoison) lib/httpoison/base.ex:394: HTTPoison.Base.request/9
       (httpoison) lib/httpoison.ex:66: HTTPoison.request!/5
       (dnsimple) lib/dnsimple.ex:123: Dnsimple.Client.execute/6
       (dnsimple) lib/dnsimple/domains_service.ex:32: Dnsimple.DomainsService.create_domain/5
       test/dnsimple_domains_service_test.exs:42

It doesn't make sense at all as the request_body: "" is there. This is actually a simplification of the real test, as I need to check the body matches the JSON-serialized version of the passed payload, but since I could not make it work I'm now trying with the simplest body possible (an empty string).

I also have one more question. Why is the request_body mandatory when I use the stub: stubbing mode? This is very counter-intuitive, as it doesn't work like that with cassettes.

  defp match_by_request_body(response, params, options) do
    if options[:stub] != nil || has_match_requests_on(:request_body, options) do
      (response[:request].body || response[:request].request_body) ==
        params[:request_body] |> to_string
    else
      true
    end
  end

I suggest to remove options[:stub] != nil and just run the match when an explicit request_body parameter is set.

Compiler warnings

I'm seeing the following using elixir 1.4:

warning: variable "adapter_method" does not exist and is being expanded to "adapter_method()", please use parentheses to remove the ambiguity or change the variable name

This happens for all of my tests which use exvcr. I have them setup as follows:

defmodule SomeApp.ModuleTest do
  use ExVCR.Mock, adapter: ExVCR.Adapter.Hackney

  setup_all do
    HTTPoison.start
  end

  test "some test that does an api call" do
    assert something == result_of_call
  end
end

My deps:

  defp deps do
    [{:phoenix, "~> 1.2.1"},
     {:phoenix_pubsub, "~> 1.0"},
     {:phoenix_ecto, "~> 3.0"},
     {:postgrex, ">= 0.0.0"},
     {:phoenix_html, "~> 2.8"},
     {:phoenix_live_reload, "~> 1.0", only: :dev},
     {:gettext, "~> 0.11"},
     {:cowboy, "~> 1.0"},
     # App specific deps
     {:comeonin, "~> 3.0"},
     {:credo, "~> 0.7", only: [:dev]},
     {:csv, "~> 1.4.2"},
     {:dialyxir, "~> 0.3.5", only: :dev},
     {:distillery, "~> 1.0", runtime: false, warn_missing: false},
     {:edeliver, "~> 1.4.0"},
     {:excoveralls, "~> 0.5", only: :test},
     {:ex_doc, "~> 0.15", only: :dev, runtime: false},
     {:ex_machina, "~> 1.0", only: :test},
     {:exvcr, "~> 0.8", runtime: false},
     {:gen_smtp, "~> 0.11.0"},
     {:guardian, "~> 0.14.2"},
     {:httpoison, "~> 0.11.0"},
     {:timex, "~> 3.0"},
     {:sweet_xml, "~> 0.6.2"},
     {:wallaby, "~> 0.14.0"},
     {:xml_builder, "~> 0.0.6"},
     {:uuid, "~> 1.1"}
    ]
  end

Provide ability to set vcr and custom folders via `config.exs`

I want to set a custom folder for the cassettes and can do this in the setup_all macro in my tests which is fine, but if I want to run the mix tasks I need to supply my custom dirs every time I run it - even though it is something that never changes.

Is it possible to set the directories in my config.exs file? That way I can create a test.exs that gets included from config.exs when in the test env and it is available for both the mix tasks and the test runner.

Thanks.

Filter request headers

Hi,

I did find an option to filter blacklisted response headers, but I could not find any option to apply the same for request headers. For example when you have api tokens in the request headers, it would be nice to be able to filter those out of the cassette as well.

Gerard.

Ignoring not matching urls for cassettes

When using HTTPoison with :hackney, I got the error of response for [URL:#{params[:url]}, METHOD:#{params[:method]}] was not found. The requests are not the same one as recorded in the cassettes so I guess that's the reason it's not working as expected.

So I propose when using ExVCR mockups, we have a switch as bypass. Just like custom, when set to true, it will ignore all the requests that are not specified in the recorded cassettes. Here's an example:

  use ExVCR.Mock, adapter: ExVCR.Adapter.Hackney
  ...hooks and other test cases...
  test "it will bypass if the cassette did not match the request" do
    use_cassette "no_match", custom: true, bypass: true do
         HTTPoison.start
         assert HTTPoison.get("http://not_match_cassette.com").status_code == 200
     end
  end

By combining the custom switch and the bypass switch, we now have the request to be passed directly to the server instead of using a pre-recorded one.

See PR #97 for details. Let me know if you have any question or concern regarding to it. Any discussion is welcome.

JSON serialization => type losses

I had issues using ExVCR because of transformations that have been made on the request / responses to save them. The JSON deserialization made all the strings as Elixir's String (no character list anymore) and my oauth lib (tim/erlang-oauth), expected a character list :-( Do you see where my problem is?

Why not using an Erlang representation instead of JSON? You'll avoid many conversions...
I tried this idea here if you're interested: https://github.com/nicoolas25/exvcr

Thank you for this work. With my minor adjustments it is very usefull!

Return value from test in use_cassette macros

Thanks for the great library!

It'd be really useful to be able to return the value from the body of the use_cassette macro, e.g.:

defmacro use_cassette(:stub, options, test) do
  quote do
    stub_fixture = "stub_fixture_#{ExVCR.Util.uniq_id}"
    stub = prepare_stub_record(unquote(options), adapter_method)
    recorder = Recorder.start([fixture: stub_fixture, stub: stub, adapter: adapter_method])

    mock_methods(recorder, adapter_method)

    try do
      value = unquote(test)
      if options_method[:clear_mock] || unquote(options)[:clear_mock] do
        :meck.unload(adapter_method.module_name)
      end
      value
    after
      # do nothing
    end
  end
end

If this seems reasonable, I'll submit a patch.

ExVCR.RequestNotMatchError - Ignore / bypass the request not matching recorded cassette

This happens when we have multiple requests in the test suite

  ** (EXIT from #PID<0.3049.0>) an exception was raised:
         ** (ExVCR.RequestNotMatchError) Request did not match with any one in the current cassette: test/fixture/vcr_cassettes/response.json.
     Delete the current cassette with [mix vcr.delete] and re-record.

     Request: [:get, "https://example.com", [], "", []]

Its clear from the above exception that, there are two requests. My test suite suppose to catch the second request and match the cassette response.

Would it be possible to do following

Ignore the request response if it does not match the cassette name use_cassette "contentful-cache"

In my case, its clear that the first response would not match the my cassette response at all.... in this case, if I could do bypass the request until I find the request that matches my recorded response then we would not have inconsistent behaviour with cassettes..

I have seen multiple issues raised here

#96
#81
#53

When we run the tests independently, they work fine.
Ex: mix test test/model_test.exs

However, when we run the entire test suite
Ex: mix test

we would have this issue with multiple requests not matching the cassettes.

Hackney Converter: Options sanitization fails

When doing a request using hackney I get the following error:

** (FunctionClauseError) no function clause matching in anonymous fn/1 in ExVCR.Adapter.Hackney.Converter.sanitize_options/1
     stacktrace:
       lib/exvcr/adapter/hackney/converter.ex:35: anonymous fn(:with_body) in ExVCR.Adapter.Hackney.Converter.sanitize_options/1
       (elixir) lib/enum.ex:1184: Enum."-map/2-lists^map/1-0-"/2
       lib/exvcr/adapter/hackney/converter.ex:29: ExVCR.Adapter.Hackney.Converter.request_to_string/1
       lib/exvcr/adapter/hackney/converter.ex:6: ExVCR.Adapter.Hackney.Converter.convert_to_string/2
       lib/exvcr/handler.ex:137: ExVCR.Handler.get_response_from_server/2
       (hackney) :hackney.request(:get, "...", [...], "", [:with_body, {:max_body, 30000000}])

Error is happening because the function sanitize_options/1, here, is expecting a list of tuples, but hackney also accepts single atoms as values.

filter_sensitive_data in query params

First off: great library, thank you.

I find myself wanting to filter out sensitive data in the query params of a url I'm recording AND still use match_requests_on: [:query]. So basically I want to match on URL but substitute false values for real ones. So I could match a recording with url http://example.com?api_key=FAKE_KEY&page=1 when fetching url http://example.com?api_key=abcdefg&page=1

It would not match a cassette with url http://example.com?api_key=abcdefg&page=SOMETHING_ELSE

Is this feature something you'd adopt? Any advice? I can try to put up a PR, but don't want to waste the time if you wouldn't find it valuable.

Option to remove headers

It looks like filter_sensitive_data will filter header values pretty well, but what about an option to filter out certain headers from the response?

Something like:

ExVCR.Config.remove_response_headers(["Content-Type", "Content-Length", "X-Random-Custom-Header"])

Add a delay to response?

I'm wondering if there's any ability to add a delay before ExVCR sends its response to the HTTP Client?

I'm trying to track down a bug where I'm expecting a timeout to happen, but the timeout is not occurring -- it would be helpful to replicate this situation using an ExVCR cassette where I could have ExVCR not respond for some configurable amount of time.

I glanced through the ExVCR code and didn't see anywhere obvious where this sort of functionality was already in place, so I'm wondering if adding it would be useful for other people? I also understand trying to trigger timeouts really isn't ExVCR's job, so I'm unsure if adding that feature would be a bad idea, but I thought I might take a crack at it and send a PR otherwise.

ExVCR.Adapter.Hackney doesn't work with head requests

It seems that ExVCR.Adapter.Hackney doesn't work with head requests.

If I use HTTPoison.head(url, headers, options) inside of use_cassette block, the relevant test fails with something like:

** (FunctionClauseError) no function clause matching in ExVCR.Adapter.Hackney.apply_filters/1
     stacktrace:
       lib/exvcr/adapter/hackney.ex:54: ExVCR.Adapter.Hackney.apply_filters({:ok, 301, [{"Server", "nginx"}, {"Date", "Thu, 24 Nov 2016 16:19:29 GMT"}, {"Content-Type", "text/html; charset=UTF-8"}, {"Connection", "keep-alive"}, {"Location", "https://dribbble.com/jobs/11990?source=feed"}, {"X-Cache", "MISS"}]})
       lib/exvcr/handler.ex:136: ExVCR.Handler.get_response_from_server/2
       (hackney) :hackney.request(:head, "https://somesite.com/some-path", [], "", [:insecure])
       lib/httpoison/base.ex:422: HTTPoison.Base.request/9

Any ideas how this could be fixed/patched? I am presuming that should be something simple, as long as one knows the internals...

ExVCR and Pavlov function adapter_method/0 undefined

Hi,

I tried to use ExVCR with Pavlov.
Both works separately but together i always get something like function adapter_method/0 undefined

Here is a code example:

defmodule MyModuleTest do
  use ExVCR.Mock, adapter: ExVCR.Adapter.Hackney
  use Pavlov.Case, async: true
  import Pavlov.Syntax.Expect

  describe « .something" do
    context "given valid params » do
      it "returns response body » do
        use_cassette "sucess » do
          response = MyModule.test
          expect response |> not_to_eq %{}
        end
      end
    end
  end
end

Any idea what i'm missing ?

Running `mix vcr` without custom cassette folder gives annoying message

If you do not create the custom_cassette folder in the same level as your cassete folder, running mix vcr will print an annoying message like this in the end:

** (ExVCR.PathNotFoundError) Specified path 'fixture/custom_cassettes' for reading cassettes was not found.
    lib/exvcr/task/runner.ex:34: ExVCR.Task.Runner.find_json_files/1
    lib/exvcr/task/runner.ex:23: ExVCR.Task.Runner.read_cassettes/1
    lib/exvcr/task/runner.ex:17: anonymous fn/1 in ExVCR.Task.Runner.show_vcr_cassettes/1
    (elixir) lib/enum.ex:604: Enum."-each/2-lists^foreach/1-0-"/2
    (elixir) lib/enum.ex:604: Enum.each/2
    (mix) lib/mix/cli.ex:58: Mix.CLI.run_task/2

My only configuration for exvcr is ExVCR.Config.cassette_library_dir("fixture/vcr_cassettes").

IMHO, if the custom cassette folder is not found it should just be ignored.

[Q] Tests run in iex session

Probably a stupid-simple thing I've missed, but can't get the tests to stop running in the iex -S mix session every time I save a file… :)

Running mix test.watch in another terminal, but this occurs even when just running iex.

Here's the mix.exs file:

defmodule MyApp.Mixfile do
  use Mix.Project

  def project do
    [
      app: :my_app,
      version: "0.1.0",
      elixir: "~> 1.4.0",
      build_embedded: Mix.env == :production,
      start_permanent: Mix.env == :production,
      deps: deps(),
      preferred_cli_env: [
        vcr: :test, "vcr.delete": :test, "vcr.check": :test, "vcr.show": :test
      ],
    ]
  end

  defp deps do
    [
      ...
      {:httpoison, "~> 0.11.0"},
    ]
    ++ devdeps()
    ++ testdeps()
  end

  defp devdeps do
    [
      ...
      {:mix_test_watch, "~> 0.2", only: :dev},
    ]
  end

  defp testdeps do
    [
      ...
      {:exvcr, "~> 0.8", only: [:ci, :test], runtime: false},
    ]
  end
end

Let me know if someone need to see the config stuff as well.

Removing exvcr stops this behaviour.

mix tasks doesn't take into account "cassette_library_dir" config

Hi,

If I set the cassette_library_dir directly into a module or inside a Mix.Config, the new directory is used when running the tests; however, it seems it is disregarded by the "mix" tasks.

I have set "preferred_cli_env:". Do you think I'm missing something or it is a general issue?

Everything seems fine by using the default dir - "fixture/vcr_cassettes". I'm trying to set it globally to "test/fixtures/vcr_cassettes".

Matching on request body

Just opening an issue to start discussion of this feature. Is it something you've thought of? Would you be open to a PR?

Error String.Chars not implemented for :multipart in :hackney

I have a somewhat complicated HTTPoision i.e :hackney POST i'm doing:

HTTPoison.post!("/photos", {:multipart, [{:file, "path/to/file", { ["form-data"], [name: "\"photo\"", filename: "\"path/to/file\""]},[]}]}, [], [recv_timeout: 30000])

and this works when I run the test without VCR, but when I run through VCR I get this error:

** (Protocol.UndefinedError) protocol String.Chars not implemented for {:multipart, [{:file, "/path/to/file", {["form-data"], [name: "\"photo\"", filename: "\"/path/to/file\""]}, []}]}
     stacktrace:
       (elixir) lib/string/chars.ex:3: String.Chars.impl_for!/1
       (elixir) lib/string/chars.ex:17: String.Chars.to_string/1
       lib/exvcr/adapter/hackney/converter.ex:6: ExVCR.Adapter.Hackney.Converter."parse_request_body (overridable 1)"/1
       lib/exvcr/adapter/hackney.ex:40: ExVCR.Adapter.Hackney.generate_keys_for_request/1
       lib/exvcr/handler.ex:22: ExVCR.Handler.get_response_from_cache/2
       lib/exvcr/handler.ex:13: ExVCR.Handler.get_response/2
       (hackney) :hackney.request(:post, ...}, [recv_timeout: 30000])
       (httpoison) lib/httpoison/base.ex:394: HTTPoison.Base.request/9
       ...

Without knowing much of the code, maybe instead of to_string we use IO.inspect here https://github.com/parroty/exvcr/blob/master/lib/exvcr/adapter/hackney/converter.ex#L61

Unable to use specific query parameters with custom cassettes

There seems to be an issue with the Regex pattern matching in the handler.ex's matching
in which URLs that contain query paramters aren't able to be used (params after the ? will break the match). Here's an example:

test "handles failure" do
  use_cassette "failure", custom: true do
    case HTTPoison.post("https://myurl.com?auth_token=123abc", "", [], []) do
      {:ok, %HTTPoison.Response{status_code: 204}} ->
        Logger.debug("Success")
      {:error, %HTTPoison.Error{id: _, reason: reason}} ->
        Logger.error("Failure")
    end
  end    
end

failure.json:

[
  {
    "request": {
      "body": "",
      "headers": {
        "Content-Type": "application/json"
      },
      "method": "post",
      "options": {
        "connect_timeout": 5000
      },
      "url": "https://myurl.com?auth_token=123abc"
    },
    "response": {
      "body": "",
      "headers": {
      },
      "status_code": 400,
      "type": "error"
    }
  }
]

A work-around is to use regex in the JSON to ignore the query params, or to simply leave the query params off:
failure.json:

[
  {
    "request": {
      "body": "",
      "headers": {
        "Content-Type": "application/json"
      },
      "method": "post",
      "options": {
        "connect_timeout": 5000
      },
      "url": "https://myurl.com.+"
    },
    "response": {
      "body": "",
      "headers": {
      },
      "status_code": 400,
      "type": "error"
    }
  }
]

But, this can be problematic if you need to ensure the query params are set correctly. Adding a Regex.escape (see below) seems to resolve the issue for this case, but looks like it breaks other pattern matching in URLs within the JSON:

pattern = Regex.compile!("^#{Regex.escape(response[:request].url)}.*$")

Suppress first request or mock a couple of requests from single cassette

Hello,

The situation is that prior to performing request under test library must obtain a token from the service provider (do an OAuth authentication), which basically means that there will always be 2 requests.
Is it possible to ignore this first request or mock it within same cassette?

mocks seem to persist outside of use_cassette blocks

Hey thanks for bringing VCR to Elixir!

I noticed that if I have tests that use a VCR cassette that other tests in the module that hit the same URL are being intercepted by the mocks even though they aren't wrapped in use_cassette blocks.

For example https://github.com/adamkittelson/simplex/blob/feature/metadata-credentials/test/simplex_test.exs#L72 and https://github.com/adamkittelson/simplex/blob/feature/metadata-credentials/test/simplex_test.exs#L54 both cause https://github.com/adamkittelson/simplex/blob/feature/metadata-credentials/lib/simplex/config.ex#L50 to be executed. If the test without a cassette happens to be executed after the one with the cassette it will be intercepted by the mock causing the test to fail.

I'm not sure if this is a bug or just the way VCR works. I've worked around it for now by calling :meck.unload/1 after the assertions using the vcr cassette.

Annoying logs

Is there a way to turn off these logs?

11:47:30.279 [error] Task #PID<0.2497.0> started from #PID<0.2495.0> terminating
** (ExVCR.RequestNotMatchError) Request did not match with any one in the current cassette: test/fixture/vcr_cassettes/geo_go_to_service.json.
Delete the current cassette with [mix vcr.delete] and re-record.

Request: [:post, "https://hooks.slack.com/services/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", [], {:form, [payload: "{\"text\":\"La tarea con ID 39310 esta en el estado arrived\\n\",\"channel\":\"#test_services\"}"]}, []]

    lib/exvcr/handler.ex:155: ExVCR.Handler.raise_error_if_cassette_already_exists/2
    lib/exvcr/handler.ex:137: ExVCR.Handler.get_response_from_server/2
    (hackney) :hackney.request(:post, "https://hooks.slack.com/services/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", [], {:form, [payload: "{\"text\":\"La tarea con ID 39310 esta en el estado arrived\\n\",\"channel\":\"#test_services\"}"]}, [])
    (httpoison) lib/httpoison/base.ex:402: HTTPoison.Base.request/9
    (elixir) lib/task/supervised.ex:85: Task.Supervised.do_apply/2
    (stdlib) proc_lib.erl:247: :proc_lib.init_p_do_apply/3
Function: &HTTPoison.post/2
    Args: ["https://hooks.slack.com/services/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", {:form, [payload: "{\"text\":\"La tarea con ID 39310 esta en el estado arrived\\n\",\"channel\":\"#test_services\"}"]}]
11:47:30.280 [error] Task #PID<0.2496.0> started from #PID<0.2495.0> terminating
** (ExVCR.RequestNotMatchError) Request did not match with any one in the current cassette: test/fixture/vcr_cassettes/geo_go_to_service.json.
Delete the current cassette with [mix vcr.delete] and re-record.

Requests to the same endpoint but with different params are treated as the same endpoint

I'm not sure if it's 100% precisely the issue, but most likely the requests to the same endpoint having different query params are considered as the same one and cache from the first response is reused for each of them.

In one of the projects I was recording cassettes for autopagination based on the links returned in headers. So I was starting with http://example.com/bookings url and for the next page http://example.com/bookings?page=2 link was returned. However, another request wasn't performed to /bookings?page=2 but the response for /bookings was reused, causing the infinite recursion (because http://example.com/bookings?page=2 was always returned as the link to the next page).

I'm investigating the issue closer, but I think the problem is in ExVCR.Handler.get_response_from_cache.

EXVCR and Espec integration

I am having some problems in integrating exvcr with espec.
I did not find a clear doc. on how to integrate and adapting what provided by the documentation
Is ESpec supported as testing framework ?

Compression problems / problems with gzip

The following test fails

  test "test use_casette with HTTPotion and gzip compression" do
    use_cassette "api_stackexchange_com" do
      HTTPotion.start
      response = :zlib.gunzip(HTTPotion.get("https://api.stackexchange.com/2.2").body)
      assert response =~ "bad_parameter"
    end
  end

with the following error

  1) test test use_casette with HTTPotion and gzip compression (DevQuotes.DataSourceControllerTest)
     test/controllers/data_source_controller_test.exs:36
     ** (ErlangError) erlang error: :data_error
     stacktrace:
       :zlib.call/3
       :zlib.inflate/2
       :zlib.gunzip/1
       lib/exvcr/json.ex:24: ExVCR.JSON.gunzip_recording/1
       (elixir) lib/enum.ex:1184: Enum."-map/2-lists^map/1-0-"/2
       lib/exvcr/json.ex:11: ExVCR.JSON.save/2
       test/controllers/data_source_controller_test.exs:37: (test)

Any idea on how to fix this?

Erro with Meck

I have tried to use but I have had this error.

  1) test post events (Keenex.Events.Test)
     test/keenex/events_test.exs:12
     ** (CaseClauseError) no case clause matching: {:error, {:undef, [{:cover, :is_compiled, [:ibrowse], []}, {:meck_proc, :get_cover_state, 1, [file: 'src/meck_proc.erl', line: 392]}, {:meck_proc, :backup_original, 3, [file: 'src/meck_proc.erl', line: 347]}, {:meck_proc, :init, 1, [file: 'src/meck_proc.erl', line: 202]}, {:gen_server, :init_it, 6, [file: 'gen_server.erl', line: 328]}, {:proc_lib, :init_p_do_apply, 3, [file: 'proc_lib.erl', line: 239]}]}}
     stacktrace:
       src/meck_proc.erl:111: :meck_proc.set_expect/2
       src/meck.erl:227: :meck.expect/3
       (elixir) lib/enum.ex:537: Enum."-each/2-lists^foreach/1-0-"/2
       (elixir) lib/enum.ex:537: Enum.each/2
       test/keenex/events_test.exs:13

This project tests also gave the same problem.

cassettes with multiple requests to URLs that only differ by query params don't seem to match on query params

I have a cassette with two request-response pairs, and the two requests have similar URLs (the first request doesn't contain query params, the second does), however, exvcr seems to always match the first request.

So, something like this in my cassette's json:

{
  "request": {
    "url": "http://test"
  },
  "response": {
    "body": "no url params"
  }
},
{
  "request": {
    "url": "http://test?param=value
  },
  "response": {
    "body": "got url params"
  }
}

When running the test, the response body for two subsequent calls is
http://test -> no url params
http://test?param=value -> no url params

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.