Skip to content

Cookbook: Record and reply requests (tape, vcr)

James Herdman edited this page May 20, 2020 · 3 revisions

Run tests against real endpoints for the first time, save responses into test/tapes directory and then read from disk on next run.

# config/test.exs
config :tesla, MyClient, adapter: Tesla.Adapter.Tape
defmodule Tesla.Adapter.Tape do
  @behaviour Tesla.Adapter

  def call(env, opts) do
    dir = opts[:dir] || "test/tapes"
    adapter = opts[:adapter] || Tesla.Adapter.Httpc
    file = Path.join(dir, key(env))

    case read(file) do
      {:ok, {:ok, status, headers, body}} ->
        {:ok, %{env | status: status, headers: headers, body: body}}

      {:ok, {:error, reason}} ->
        {:error, reason}

      {:error, :enoent} ->
        response = adapter.call(env, [])
        File.mkdir_p(dir)
        write(file, response)
        response
    end
  end

  defp key(env) do
    [
      key_method(env.method),
      key_url(env.url, env.query),
      key_headers(env.headers),
      key_body(env.body)
    ]
    |> Enum.filter(& &1)
    |> Enum.join("_")
  end

  defp hash(string), do: Base.encode16(:crypto.hash(:md5, string))

  defp stringify(a) when not is_bitstring(a), do: a.__struct__.to_string(a)
  defp stringify(a), do: a

  defp key_headers(headers) do
    headers
    |> Enum.map(fn {a, b} -> a <> stringify(b) end)
    |> Enum.join()
    |> encrypt()
  end
  defp key_method(method), do: String.upcase(to_string(method))
  defp key_url(url, query), do: String.replace(Tesla.build_url(url, query), ~r/[^a-z0-9]+/i, "_")
  defp key_body(nil), do: nil
  defp key_body(""), do: nil
  defp key_body(body), do: hash(body)

  defp read(file) do
    case File.read(file) do
      {:ok, data} -> {:ok, :erlang.binary_to_term(data)}
      {:error, :enoent} -> {:error, :enoent}
    end
  end

  defp write(file, response) do
    data =
      case response do
        {:ok, %{status: status, headers: headers, body: body}} -> {:ok, status, headers, body}
        {:error, reason} -> {:error, reason}
      end

    File.write(file, :erlang.term_to_binary(data))
  end
end