Skip to content

Instantly share code, notes, and snippets.

@dbernheisel
Last active November 18, 2023 13:43
Show Gist options
  • Select an option

  • Save dbernheisel/fe9882a8cc03f950b2ad8b150f8547d0 to your computer and use it in GitHub Desktop.

Select an option

Save dbernheisel/fe9882a8cc03f950b2ad8b150f8547d0 to your computer and use it in GitHub Desktop.
Req adapter that raises while in MIX_ENV=test
defmodule MyApp.Req do
@moduledoc """
Wrapper around Req to apply defaults. See [Req](https://hexdocs.pm/req)
This will also protect from external requests running while MIX_ENV=test.
To set the adapter in controller or integration tests, you may configure
your mock with `Process.put(:req_adapter, my_mock)`.
You must specify `external_service` and `request_name` telemetry tags
These will be used in `:telemetry` handlers.
"""
defguardp is_uri(uri) when is_binary(uri) or is_struct(uri, Uri)
defmodule Adapter do
if Mix.env() == :test do
# For when using Bypass, let it through
def call(%{url: %{host: "localhost"}} = request), do: Req.Steps.run_finch(request)
# Otherwise, raise it.
def call(request) do
raise """
Detected external API call in MIX_ENV=test to "#{request.url}".
Please provide an `:adapter` option to MyApp.Req and provide a mock.
See https://hexdocs.pm/req/Req.Request.html#module-adapter for more information.
tldr: provide a function that takes the request and returns a tuple of request and response:
{request, Req.Response.new() |> add_my_mock()}
To set the adapter in controller or integration tests where you can't access the callsite
to provide the mock, you may place it into the Process dictionary with:
Process.put(:req_adapter, my_mock)
"""
end
else
def call(request), do: Req.Steps.run_finch(request)
end
end
def get(opts) when is_list(opts), do: opts |> Keyword.put(:method, :get) |> request()
def get(uri) when is_uri(uri), do: request(uri, method: :get)
def get(uri, opts) when is_uri(uri), do: request(uri, Keyword.put(opts, :method, :get))
def get!(opts) when is_list(opts), do: opts |> Keyword.put(:method, :get) |> request!()
def get!(uri) when is_uri(uri), do: request!(uri, method: :get)
def get!(uri, opts) when is_uri(uri), do: request!(uri, Keyword.put(opts, :method, :get))
def put(opts), do: opts |> Keyword.put(:method, :put) |> request()
def put(uri, body, opts \\ []) when is_uri(uri) do
request(uri, opts |> put_body(body) |> Keyword.put(:method, :put))
end
def put!(opts), do: opts |> Keyword.put(:method, :put) |> request!()
def put!(uri, body, opts \\ []) when is_uri(uri) do
request!(uri, opts |> put_body(body) |> Keyword.put(:method, :put))
end
def post!(opts), do: opts |> Keyword.put(:method, :post) |> request!()
def post!(uri, body, opts \\ []) when is_uri(uri) do
request!(uri, opts |> put_body(body) |> Keyword.put(:method, :post))
end
def post(opts), do: opts |> Keyword.put(:method, :post) |> request()
def post(uri, body, opts \\ []) when is_uri(uri) do
request(uri, opts |> put_body(body) |> Keyword.put(:method, :post))
end
def request!(uri, opts) when is_uri(uri), do: opts |> Keyword.put(:url, uri) |> request!()
def request(uri, opts) when is_uri(uri), do: opts |> Keyword.put(:url, uri) |> request()
def request!(opts) do
opts
|> put_adapter()
|> require_telemetry()
|> Req.request!()
end
def request(opts) do
opts
|> put_adapter()
|> require_telemetry()
|> Req.request()
end
defp put_body(opts, body) when is_binary(body), do: Keyword.put_new(opts, :body, body)
defp put_body(opts, body) when is_map(body), do: Keyword.put_new(opts, :json, body)
# This will handle the case when the body is included in the options already, and no body arg is provided
# In this scenario, the options are provided as the body arg, so we'll flip it here.
defp put_body([], opts) when is_list(opts), do: opts
# Avoid penalty of checking process dictionary during runtime
if Mix.env() == :test do
defp put_adapter(opts) do
Keyword.put_new_lazy(opts, :adapter, fn ->
Process.get(:req_adapter, &__MODULE__.Adapter.call/1)
end)
end
else
defp put_adapter(opts) do
Keyword.put_new(opts, :adapter, &__MODULE__.Adapter.call/1)
end
end
defp require_telemetry(opts) do
{external_service, opts} = Keyword.pop(opts, :external_service)
external_service ||
raise """
You must supply `:external_service` to the Req request.
This is used to emit telemetry and represents the overall external service,
eg, "Google", that is being requested. Please ensure it's a consistent and
controlled value, not sourced from user input.
"""
{request_name, opts} = Keyword.pop(opts, :request_name)
request_name ||
raise """
You must supply `:request_name` to the Req request.
This is used to emit telemetry and represents the overall the nature of the
request external service, eg, "get_latest". Please ensure it's a consistent
and controlled value, not sourced from user input.
"""
default = [request_name: request_name, external_service: external_service]
Keyword.update(opts, :finch_private, default, fn private ->
private
|> Map.new()
|> Map.put(:request_name, request_name)
|> Map.put(:external_service, external_service)
end)
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment