lib/salemove/http_client/test/http_client_case.ex

defmodule Salemove.HttpClientCase do
  @moduledoc """
  This case template can be used to easily mock Tesla responses
  without messing up manually with `Tesla.Env` using helper
  `allow_http_request/1` and `assert_requested/2` macro.

  Client adapter must be set to `Tesla.Mock`.

  ## Example

      # in config/test.exs

      config :my_app, GithubClient,
        base_url: "https://api.github.com/",
        adapter: Tesla.Mock

      defmodule GithubClient do
        use Salemove.HttpClient, Application.get_env(:my_app, __MODULE__)
      end

      defmodule GithubClientTest do
        use Salemove.HttpClientCase

        test "sends request" do
          allow_http_request fn env ->
            env
            |> status(200)
            |> json(%{status: "ok"})
          end

          assert {:ok, %Salemove.HttpClient.Response{body: response}} = GithubClient.get("/")

          assert_requested %{url: "https://api.github.com/"}
        end
      end
  """

  use ExUnit.CaseTemplate

  using do
    quote do
      import Salemove.HttpClientCase
    end
  end

  @doc """
  Setup mock for the current test
  """
  def allow_http_request(mock_func) do
    Tesla.Mock.mock(fn env ->
      notify_calling_process(env)
      mock_func.(env)
    end)
  end

  @doc """
  Verify that previously set up HTTP mock has been called
  """
  defmacro assert_requested(pattern, timeout \\ 100)

  defmacro assert_requested({:when, _, [pattern, guard]}, timeout) do
    quote do
      assert_receive({:http_request, unquote(pattern)} when unquote(guard), unquote(timeout))
    end
  end

  defmacro assert_requested(pattern, timeout) do
    quote do
      assert_receive {:http_request, unquote(pattern)}, unquote(timeout)
    end
  end

  @doc """
  Set status for mocked response
  """
  def status(%Tesla.Env{} = env, status) do
    %{env | status: status}
  end

  @doc """
  Set body (as string) for mocked response
  """
  def body(%Tesla.Env{} = env, body) do
    %{env | body: body}
  end

  @doc """
  Set JSON body for mocked response
  """
  def json(%Tesla.Env{} = env, body) do
    env
    |> header("content-type", "application/json")
    |> body(Jason.encode!(body))
  end

  @doc """
  Add arbitrary header to the mocked response
  """
  def header(%Tesla.Env{} = env, name, value) do
    Tesla.put_header(env, name, value)
  end

  defp notify_calling_process(env) do
    send(self(), {:http_request, env})
    env
  end
end