lib/charon/test_helpers.ex

defmodule Charon.TestHelpers do
  @moduledoc """
  Utility functions for writing tests.
  """
  alias Plug.Conn
  alias Charon.{Config, Utils, SessionPlugs, Models}
  alias Models.{Session, Tokens}
  use Charon.Internal.Constants

  @type test_session :: %{session: Session.t(), tokens: Tokens.t(), cookies: Conn.resp_cookies()}

  @doc """
  Override configuration for an optional module.
  """
  @spec override_opt_mod_conf(Config.t(), atom | binary(), map | keyword()) ::
          Config.t()
  def override_opt_mod_conf(config, module, overrides) do
    opt_mods = config.optional_modules
    mod_conf = opt_mods |> Map.get(module, %{}) |> Map.merge(Map.new(overrides))
    opt_mods = Map.merge(opt_mods, %{module => mod_conf})
    %{config | optional_modules: opt_mods}
  end

  @doc """
  Create a test session and return the session, a set of tokens for it and the response cookies.

  ## Options

    - `:upsert_session_opts` (default `[]`) as defined in `t:Charon.SessionPlugs.upsert_session_opts/0`
  """
  @spec create_session(Config.t(), SessionPlugs.upsert_session_opts()) :: test_session
  def create_session(config, upsert_session_opts \\ []) do
    Plug.Test.conn(:get, "/")
    |> SessionPlugs.upsert_session(
      config,
      Keyword.merge([token_transport: :bearer], upsert_session_opts)
    )
    |> then(fn conn ->
      %{
        session: Utils.get_session(conn),
        tokens: Utils.get_tokens(conn),
        cookies: conn.resp_cookies
      }
    end)
  end

  @doc """
  Create a new test session and put a token (and its cookie when needed) on the conn.
  This is essentially `create_session/3` and `put_token_for/3` combined into one
  convenience function.

  ## Options

    The upsert_session_opts defined in `t:Charon.SessionPlugs.upsert_session_opts/0` AND
    - `:token` is either `:access` (default) or `:refresh`
  """
  @spec put_token(Conn.t(), Config.t(), keyword) :: Conn.t()
  def put_token(conn, config, opts \\ []) do
    test_session = create_session(config, Keyword.drop(opts, [:token]))
    put_token_for(conn, test_session, Keyword.take(opts, [:token]))
  end

  @doc """
  Put a token (and its cookie when needed) from the test session on the conn.
  Use `create_session/3` to create the test session.

  ## Options

    - `:token` is either `:access` (default) or `:refresh`
  """
  @spec put_token_for(Conn.t(), test_session(), token: :access | :refresh) :: Conn.t()
  def put_token_for(conn, test_session, opts \\ []) do
    token = resolve_token_type(opts)
    %{cookies: cookies, tokens: %{^token => token}} = test_session
    conn |> put_bearer_token(token) |> put_req_cookies(cookies)
  end

  ###########
  # Private #
  ###########

  defp put_bearer_token(conn, token) do
    Conn.put_req_header(conn, "authorization", "Bearer #{token}")
  end

  defp put_req_cookies(conn, cookies) do
    Enum.reduce(cookies, conn, fn {k, v}, c -> Plug.Test.put_req_cookie(c, k, v.value) end)
  end

  defp resolve_token_type(opts) do
    token = opts[:token] || :access
    Map.fetch!(%{access: :access_token, refresh: :refresh_token}, token)
  end
end