lib/storage/test_storage.ex

defmodule Uploadex.TestStorage do
  @moduledoc """
  Storage to be used for testing. It holds the files in memory (using a `Agent`).

  ## Configuring

  To configure your app to use the `Uploadex.TestStorage`, just add this storage to your `Uploadex` module,
  depending on the current environment:

      defmodule MyApp.Uploader do
        use Uploadex

        @impl true
        def get_fields(%User{}), do: :files

        @impl true
        def default_opts(Uploadex.FileStorage), do: [...]
        def default_opts(Uploadex.TestStorage), do: []

        @impl true
        def storage(%User{}, _field) do
          if test_environment?() do
            {Uploadex.TestStorage, []}
          else
            {Uploadex.FileStorage, [...]}
          end
        end

        @impl true
        def accepted_extensions(%User{}, _field), do: ~w(.jpg .png)

        defp test_environment?() do
          # This env must be set in your config files depending on the environment.
          Application.fetch_env!(:my_app, :environment) == :test
        end
      end

  ## Using

  In your `ExUnit` tests, add a `setup` block starting the storage:

      setup do
        Uploadex.TestStorage.start_link()
        :ok
      end

  Then, in the tests, you can use the `get_stored/1`, `get_deleted/1` and `get_opts`:

      test "some test with files" do
        assert ["document-1.pdf", "document-2.pdf"] == Uploadex.TestStorage.get_stored()
        assert ["deleted-document.pdf"] == Uploadex.TestStorage.get_deleted()
        assert [] == Uploadex.TestStorage.get_opts()
      end

  This module is just the base for testing, check `Uploadex.Testing` for a more convenient way to test uploads.
  """

  use Agent

  @behaviour Uploadex.Storage

  def start_link(initial_state \\ %{}) when is_map(initial_state) do
    agent_name = initial_state |> Map.get(:opts, []) |> get_agent_name()

    default_state = %{stored: [], deleted: [], opts: [agent_name: agent_name]}
    initial_state = Map.merge(default_state, Map.new(initial_state))

    Agent.start_link(fn -> initial_state end, name: agent_name)
  end

  def get_stored(opts \\ []) do
    agent_get(:stored, opts)
  end

  def get_deleted(opts \\ []) do
    agent_get(:deleted, opts)
  end

  def get_opts(opts \\ []) do
    agent_get(:opts, opts)
  end

  @impl true
  def store(file, opts) do
    update_fun = fn state ->
      state
      |> Map.update!(:stored, &(&1 ++ [file]))
      |> Map.update!(:opts, &Keyword.merge(&1, opts))
    end

    agent_update(update_fun, opts)
  end

  @impl true
  def delete(file, opts) do
    update_fun = fn state ->
      state
      |> Map.update!(:deleted, &(&1 ++ [file]))
      |> Map.update!(:opts, &Keyword.merge(&1, opts))
    end

    agent_update(update_fun, opts)
  end

  @impl true
  def get_url(%{filename: filename}, _opts), do: {:ok, filename}
  def get_url(filename, _opts) when is_binary(filename), do: {:ok, filename}

  @impl true
  def get_temporary_file(%{filename: filename}, _path, _opts), do: filename
  def get_temporary_file(filename, _path, _opts) when is_binary(filename), do: filename

  defp agent_get(state_field, opts) when is_atom(state_field) do
    opts
    |> get_agent_name()
    |> Agent.get(fn state -> Map.fetch!(state, state_field) end)
  end

  defp agent_update(update_fun, opts) when is_function(update_fun) do
    opts
    |> get_agent_name()
    |> Agent.update(update_fun)
  end

  defp get_agent_name(opts) do
    current_pid_as_atom = self() |> inspect() |> String.to_atom()

    opts[:agent_name] || current_pid_as_atom
  end
end