lib/one_piece/commanded/test_support/command_handler_case.ex

defmodule OnePiece.Commanded.TestSupport.CommandHandlerCase do
  @moduledoc ~S"""
  This module helps with test cases for testing aggregate states, and command handlers.

  ## Usage

  After import the test support file, you should be able to have the module in your test files.

      defmodule MyAggregateTest do
        use OnePiece.Commanded.TestSupport.CommandHandlerCase,
          aggregate: MyAggregate,
          # You can also pass `handler` if you are using Command Handler modules
          # handler: MyCommandHandler,
          async: true

        describe "my aggregate" do
          test "should do something" do
            assert_events(
              [%InitialEvent{}]
              %DoSomething{},
              [%SomethingHappened{}]
            )
          end

          test "the state" do
            assert_state(
              [%InitialEvent{}]
              %DoSomething{},
              %MyAggregate{}
            )
          end

          test "the error" do
            assert_error(
              [%InitialEvent{}]
              %DoSomething{},
              :already_exists
            )
          end
        end
      end
  """

  use ExUnit.CaseTemplate

  alias Commanded.Aggregate.Multi
  alias OnePiece.Commanded.TestSupport.CommandHandlerCase

  using opts do
    quote do
      @aggregate Keyword.fetch!(unquote(opts), :aggregate)
      @handler Keyword.get(unquote(opts), :handler, nil)

      defp assert_events(initial_events, command, expected_events) do
        CommandHandlerCase.assert_events(
          initial_events,
          command,
          expected_events,
          @aggregate,
          @handler
        )
      end

      defp assert_state(initial_events, command, expected_state) do
        CommandHandlerCase.assert_state(
          initial_events,
          command,
          expected_state,
          @aggregate,
          @handler
        )
      end

      defp assert_error(initial_events, command, expected_error) do
        CommandHandlerCase.assert_error(
          initial_events,
          command,
          expected_error,
          @aggregate,
          @handler
        )
      end
    end
  end

  def assert_events(
        initial_events,
        command,
        expected_events,
        aggregate_module,
        command_handler_module
      ) do
    assert {:ok, _state, events} =
             aggregate_run(
               aggregate_module,
               command_handler_module,
               initial_events,
               command
             )

    actual_events = List.wrap(events)
    expected_events = List.wrap(expected_events)

    assert actual_events == expected_events
  end

  def assert_state(
        initial_events,
        command,
        expected_state,
        aggregate_module,
        command_handler_module
      ) do
    assert {:ok, state, _events} =
             aggregate_run(
               aggregate_module,
               command_handler_module,
               initial_events,
               command
             )

    assert state == expected_state
  end

  def assert_error(
        initial_events,
        command,
        expected_error,
        aggregate_module,
        command_handler_module
      ) do
    assert {:error, reason} =
             aggregate_run(
               aggregate_module,
               command_handler_module,
               initial_events,
               command
             )

    assert reason == expected_error
  end

  defp aggregate_run(aggregate_module, command_handler_module, initial_events, command) do
    evolver = &aggregate_module.apply/2

    decider =
      if command_handler_module == nil,
        do: &aggregate_module.execute/2,
        else: &command_handler_module.handle/2

    aggregate_module
    |> struct()
    |> evolve(initial_events, evolver)
    |> execute(command, evolver, decider)
  end

  defp execute(state, command, evolver, decider) do
    try do
      {new_state, events} =
        state
        |> decider.(command)
        |> process_response()
        |> maybe_evolve(state, evolver)

      {:ok, new_state, events}
    catch
      {:error, _error} = reply -> reply
    end
  end

  defp process_response({:error, _error} = error) do
    throw(error)
  end

  defp process_response(%Multi{} = multi) do
    case Multi.run(multi) do
      {:error, _reason} = error ->
        throw(error)

      {state, events} ->
        {state, events}
    end
  end

  defp process_response(no_events) when no_events in [:ok, nil, []] do
    []
  end

  defp process_response({:ok, events}) do
    events
  end

  defp process_response(events) when is_list(events) do
    events
  end

  defp process_response(event) when is_map(event) do
    [event]
  end

  defp process_response(invalid) do
    flunk("unexpected: " <> inspect(invalid))
  end

  defp maybe_evolve({state, events} = _multi_response, _state, _evolver) do
    {state, events}
  end

  defp maybe_evolve(events, state, evolver) do
    {evolve(state, events, evolver), events}
  end

  defp evolve(state, events, evolver) do
    events
    |> List.wrap()
    |> Enum.reduce(state, &evolver.(&2, &1))
  end
end