Last active
November 18, 2023 13:43
-
-
Save dbernheisel/fe9882a8cc03f950b2ad8b150f8547d0 to your computer and use it in GitHub Desktop.
Req adapter that raises while in MIX_ENV=test
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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