diff --git a/test/aggregates/aggregate_concurrency_test.exs b/test/aggregates/aggregate_concurrency_test.exs index 3d447896..d57ae6ab 100644 --- a/test/aggregates/aggregate_concurrency_test.exs +++ b/test/aggregates/aggregate_concurrency_test.exs @@ -3,30 +3,11 @@ defmodule Commanded.Aggregates.AggregateConcurrencyTest do alias Commanded.MockedApp alias Commanded.Aggregates.{Aggregate, ExecutionContext} - alias Commanded.EventStore.RecordedEvent alias Commanded.ExampleDomain.{BankAccount, DepositMoneyHandler, OpenAccountHandler} alias Commanded.ExampleDomain.BankAccount.Commands.{DepositMoney, OpenAccount} alias Commanded.ExampleDomain.BankAccount.Events.MoneyDeposited alias Commanded.UUID - setup do - expect(MockEventStore, :subscribe_to, fn - _event_store_meta, stream_uuid, handler_name, handler, _subscribe_from, _opts -> - assert is_binary(stream_uuid) - assert is_binary(handler_name) - - {:ok, handler} - end) - - expect(MockEventStore, :subscribe, fn _event_store_meta, aggregate_uuid -> - assert is_binary(aggregate_uuid) - - :ok - end) - - :ok - end - describe "concurrency error" do setup [:open_account] @@ -46,38 +27,20 @@ defmodule Commanded.Aggregates.AggregateConcurrencyTest do retry_attempts: 1 } - # Fail to append once - expect(MockEventStore, :append_to_stream, fn - _event_store_meta, ^account_number, 1, _event_data, _opts -> - {:error, :wrong_expected_version} - end) - - # Return "missing" event - expect(MockEventStore, :stream_forward, fn - _event_store_meta, ^account_number, 2, _batch_size -> - [ - %RecordedEvent{ - event_id: UUID.uuid4(), - event_number: 2, - stream_id: account_number, - stream_version: 2, - event_type: "Elixir.Commanded.ExampleDomain.BankAccount.Events.MoneyDeposited", - data: %MoneyDeposited{ - account_number: account_number, - transfer_uuid: UUID.uuid4(), - amount: 500, - balance: 1_500 - }, - metadata: %{} - } - ] - end) - - # Succeed on second attempt - expect(MockEventStore, :append_to_stream, fn - _event_store_meta, ^account_number, 2, _event_data, _opts -> - :ok - end) + concurrent_event = + build_recorded_event( + account_number, + 2, + %MoneyDeposited{ + account_number: account_number, + transfer_uuid: UUID.uuid4(), + amount: 500, + balance: 1_500 + }, + event_type: "Elixir.Commanded.ExampleDomain.BankAccount.Events.MoneyDeposited" + ) + + expect_concurrency_retry_succeeds(account_number, concurrent_event) assert {:ok, 3, _events, _aggregate_state} = Aggregate.execute(MockedApp, BankAccount, account_number, context) @@ -92,16 +55,7 @@ defmodule Commanded.Aggregates.AggregateConcurrencyTest do end test "should error after too many attempts", %{account_number: account_number} do - # Fail to append to stream - expect(MockEventStore, :append_to_stream, 6, fn - _event_store_meta, ^account_number, 1, _event_data, _opts -> - {:error, :wrong_expected_version} - end) - - expect(MockEventStore, :stream_forward, 6, fn - _event_store_meta, ^account_number, 2, _batch_size -> - [] - end) + expect_too_many_retry_attempts(account_number) command = %DepositMoney{ account_number: account_number, @@ -123,15 +77,7 @@ defmodule Commanded.Aggregates.AggregateConcurrencyTest do defp open_account(_context) do account_number = UUID.uuid4() - expect(MockEventStore, :stream_forward, fn - _event_store_meta, ^account_number, 1, _batch_size -> - [] - end) - - expect(MockEventStore, :append_to_stream, fn - _event_store_meta, ^account_number, 0, _event_data, _opts -> - :ok - end) + expect_open_aggregate(account_number) {:ok, ^account_number} = Commanded.Aggregates.Supervisor.open_aggregate(MockedApp, BankAccount, account_number) diff --git a/test/aggregates/aggregate_telemetry_test.exs b/test/aggregates/aggregate_telemetry_test.exs index dded409a..e01a0cc9 100644 --- a/test/aggregates/aggregate_telemetry_test.exs +++ b/test/aggregates/aggregate_telemetry_test.exs @@ -258,22 +258,7 @@ defmodule Commanded.Aggregates.AggregateTelemetryTest do test "emit `[:commanded, :aggregate, :execute, :wrong_expected_version]` event" do aggregate_uuid = UUID.uuid4() - expect(MockEventStore, :subscribe, fn _event_store_meta, aggregate_uuid -> - assert is_binary(aggregate_uuid) - :ok - end) - - expect(MockEventStore, :append_to_stream, fn _meta, - ^aggregate_uuid, - _exp_ver, - _event_data, - _opts -> - {:error, :wrong_expected_version} - end) - - expect(MockEventStore, :stream_forward, 2, fn _meta, ^aggregate_uuid, _from, _batch_size -> - [] - end) + expect_wrong_expected_version_conflict(aggregate_uuid) assert {:ok, _pid} = start_aggregate(aggregate_uuid, application: Commanded.MockedApp) @@ -320,11 +305,7 @@ defmodule Commanded.Aggregates.AggregateTelemetryTest do test "load only when stream_forward returns stream_not_found" do aggregate_uuid = UUID.uuid4() - expect(MockEventStore, :subscribe, fn _meta, ^aggregate_uuid -> :ok end) - - expect(MockEventStore, :stream_forward, fn _meta, ^aggregate_uuid, _from, _batch_size -> - {:error, :stream_not_found} - end) + expect_stream_not_found(aggregate_uuid) assert {:ok, _pid} = start_aggregate(aggregate_uuid, application: MockedApp) @@ -339,24 +320,14 @@ defmodule Commanded.Aggregates.AggregateTelemetryTest do aggregate_uuid = UUID.uuid4() count = 2 - expect(MockEventStore, :subscribe, fn _meta, ^aggregate_uuid -> :ok end) - - expect(MockEventStore, :stream_forward, fn _meta, ^aggregate_uuid, _from, _batch_size -> + events = for i <- 1..count do - %Commanded.EventStore.RecordedEvent{ - event_id: UUID.uuid4(), - event_number: i, - stream_id: aggregate_uuid, - stream_version: i, - correlation_id: nil, - causation_id: nil, - event_type: "Elixir.Commanded.Aggregates.AggregateTelemetryTest.Event", - data: %Event{message: "event#{i}"}, - metadata: nil, - created_at: DateTime.utc_now() - } + build_recorded_event(aggregate_uuid, i, %Event{message: "event#{i}"}, + event_type: "Elixir.Commanded.Aggregates.AggregateTelemetryTest.Event" + ) end - end) + + expect_stream_with_events(aggregate_uuid, events) assert {:ok, _pid} = start_aggregate(aggregate_uuid, application: MockedApp) diff --git a/test/event_handler_after_start_test.exs b/test/event_handler_after_start_test.exs index 7f91f60f..b1eee2d3 100644 --- a/test/event_handler_after_start_test.exs +++ b/test/event_handler_after_start_test.exs @@ -1,15 +1,8 @@ defmodule Commanded.Event.HandlerAfterStartTest do use Commanded.MockEventStoreCase - import Mox - - alias Commanded.EventStore.Adapters.Mock, as: MockEventStore - setup do - stub(MockEventStore, :subscribe_to, fn - _event_store, :all, _handler_name, handler, _subscribe_from, _opts -> - {:ok, handler} - end) + stub_subscribe_to_return_handler() :ok end diff --git a/test/opentelemetry/aggregate_test.exs b/test/opentelemetry/aggregate_test.exs index 8d0fe54d..3c96d1c9 100644 --- a/test/opentelemetry/aggregate_test.exs +++ b/test/opentelemetry/aggregate_test.exs @@ -5,7 +5,6 @@ defmodule Commanded.OpenTelemetry.AggregateTest do alias Commanded.Aggregates.AggregateTelemetryTest alias Commanded.Aggregates.{Aggregate, ExecutionContext} alias Commanded.DefaultApp - alias Commanded.EventStore.Adapters.Mock, as: MockEventStore alias Commanded.Middleware.Commands.IncrementCount alias Commanded.Middleware.Commands.RaiseError alias Commanded.MockedApp @@ -606,22 +605,7 @@ defmodule Commanded.OpenTelemetry.AggregateTest do test "span has wrong_expected_version event when conflict occurs" do aggregate_uuid = UUID.uuid4() - expect(MockEventStore, :subscribe, fn _event_store_meta, ^aggregate_uuid -> - assert is_binary(aggregate_uuid) - :ok - end) - - expect(MockEventStore, :append_to_stream, fn _meta, - ^aggregate_uuid, - _exp_ver, - _event_data, - _opts -> - {:error, :wrong_expected_version} - end) - - expect(MockEventStore, :stream_forward, 2, fn _meta, ^aggregate_uuid, _from, _batch_size -> - [] - end) + expect_wrong_expected_version_conflict(aggregate_uuid) assert {:ok, _pid} = start_aggregate(aggregate_uuid, application: MockedApp) @@ -673,44 +657,15 @@ defmodule Commanded.OpenTelemetry.AggregateTest do test "span has wrong_expected_version event with count when retry succeeds" do aggregate_uuid = UUID.uuid4() - expect(MockEventStore, :subscribe, fn _event_store_meta, ^aggregate_uuid -> - assert is_binary(aggregate_uuid) - :ok - end) - - expect(MockEventStore, :append_to_stream, 2, fn - _meta, ^aggregate_uuid, 0, _event_data, _opts -> - {:error, :wrong_expected_version} - - _meta, ^aggregate_uuid, 1, _event_data, _opts -> - :ok - end) + event = + build_recorded_event( + aggregate_uuid, + 1, + struct!(Commanded.Aggregates.AggregateTelemetryTest.Event, message: "event"), + event_type: "Elixir.Commanded.Aggregates.AggregateTelemetryTest.Event" + ) - stream_forward_calls = :counters.new(1, []) - - expect(MockEventStore, :stream_forward, 2, fn _meta, ^aggregate_uuid, 1, _batch_size -> - n = :counters.get(stream_forward_calls, 1) - :counters.add(stream_forward_calls, 1, 1) - - if n == 0 do - [] - else - [ - %Commanded.EventStore.RecordedEvent{ - event_id: UUID.uuid4(), - event_number: 1, - stream_id: aggregate_uuid, - stream_version: 1, - correlation_id: nil, - causation_id: nil, - event_type: "Elixir.Commanded.Aggregates.AggregateTelemetryTest.Event", - data: struct!(Commanded.Aggregates.AggregateTelemetryTest.Event, message: "event"), - metadata: nil, - created_at: DateTime.utc_now() - } - ] - end - end) + expect_wrong_expected_version_retry_succeeds(aggregate_uuid, event) assert {:ok, _pid} = start_aggregate(aggregate_uuid, application: MockedApp) @@ -760,21 +715,7 @@ defmodule Commanded.OpenTelemetry.AggregateTest do test "no wrong_expected_version event when count is 0" do aggregate_uuid = UUID.uuid4() - expect(MockEventStore, :subscribe, fn _event_store_meta, ^aggregate_uuid -> - :ok - end) - - expect(MockEventStore, :append_to_stream, fn _meta, - ^aggregate_uuid, - _exp_ver, - _event_data, - _opts -> - :ok - end) - - expect(MockEventStore, :stream_forward, fn _meta, ^aggregate_uuid, _from, _batch_size -> - [] - end) + expect_successful_append_with_empty_stream(aggregate_uuid) assert {:ok, _pid} = start_aggregate(aggregate_uuid, application: MockedApp) diff --git a/test/projections/error_callback_test.exs b/test/projections/error_callback_test.exs index 2dcaf806..27338a2d 100644 --- a/test/projections/error_callback_test.exs +++ b/test/projections/error_callback_test.exs @@ -1,11 +1,9 @@ defmodule Commanded.Projections.ErrorCallbackTest do - use ExUnit.Case + use Commanded.MockProjectionCase import Commanded.Projections.ProjectionAssertions import ExUnit.CaptureLog - import Mox - alias Commanded.EventStore.Adapters.Mock, as: MockEventStore alias Commanded.EventStore.RecordedEvent alias Commanded.Projections.Events.{ @@ -21,13 +19,6 @@ defmodule Commanded.Projections.ErrorCallbackTest do alias Commanded.UUID alias Ecto.Adapters.SQL.Sandbox - setup [:set_mox_global, :stub_event_store, :verify_on_exit!] - - setup do - start_supervised!({TestApplication, event_store: [adapter: MockEventStore]}) - Sandbox.checkout(Repo) - end - describe "error handling" do setup [:start_projector] @@ -147,21 +138,6 @@ defmodule Commanded.Projections.ErrorCallbackTest do end end - defp stub_event_store(_context) do - stub(MockEventStore, :ack_event, fn _adapter_meta, _pid, _event -> :ok end) - - stub(MockEventStore, :child_spec, fn _application, _config -> - {:ok, [], %{}} - end) - - stub(MockEventStore, :subscribe_to, fn - _event_store, :all, _handler_name, _handler, _subscribe_from, _opts -> - {:ok, self()} - end) - - :ok - end - defp start_projector(_context) do projector = start_supervised!(ErrorProjector) diff --git a/test/projections/runtime_config_projector_test.exs b/test/projections/runtime_config_projector_test.exs index bb95b0ef..cfb04a8a 100644 --- a/test/projections/runtime_config_projector_test.exs +++ b/test/projections/runtime_config_projector_test.exs @@ -1,23 +1,14 @@ defmodule Commanded.Projections.RuntimeConfigProjectorTest do - use ExUnit.Case + use Commanded.MockProjectionCase - alias Commanded.EventStore.Adapters.Mock, as: MockEventStore alias Commanded.EventStore.RecordedEvent alias Commanded.Projections.Events.AnEvent alias Commanded.Projections.{Projection, ProjectionAssertions, Repo, RuntimeConfigProjector} alias Commanded.UUID alias Ecto.Adapters.SQL.Sandbox - import Mox import ProjectionAssertions - setup [:set_mox_global, :stub_event_store, :verify_on_exit!] - - setup do - start_supervised!({TestApplication, event_store: [adapter: MockEventStore]}) - Sandbox.checkout(Repo) - end - describe "runtime config projector" do setup do projector1 = @@ -47,6 +38,7 @@ defmodule Commanded.Projections.RuntimeConfigProjectorTest do ]) assert_receive {:project, "AnEvent"} + assert_receive {:projected, "AnEvent"} assert_projections(Projection, ["AnEvent"]) assert last_seen_event("RuntimeProjector1") == 1 @@ -57,19 +49,4 @@ defmodule Commanded.Projections.RuntimeConfigProjectorTest do defp send_events(projector, events) do send(projector, {:events, events}) end - - defp stub_event_store(_context) do - stub(MockEventStore, :ack_event, fn _adapter_meta, _pid, _event -> :ok end) - - stub(MockEventStore, :child_spec, fn _application, _config -> - {:ok, [], %{}} - end) - - stub(MockEventStore, :subscribe_to, fn - _event_store, :all, _handler_name, _handler, _subscribe_from, _opts -> - {:ok, self()} - end) - - :ok - end end diff --git a/test/support/mock_event_store_case.ex b/test/support/mock_event_store_case.ex index e58aefc2..772a5297 100644 --- a/test/support/mock_event_store_case.ex +++ b/test/support/mock_event_store_case.ex @@ -7,10 +7,12 @@ defmodule Commanded.MockEventStoreCase do alias Commanded.EventStore.Adapters.Mock, as: MockEventStore alias Commanded.MockedApp + alias Commanded.TestSupport.MockEventStoreHelpers using do quote do import Mox + import Commanded.TestSupport.MockEventStoreHelpers alias Commanded.EventStore.Adapters.Mock, as: MockEventStore end @@ -25,21 +27,6 @@ defmodule Commanded.MockEventStoreCase do end def stub_event_store(_context) do - stub(MockEventStore, :ack_event, fn _event_store_meta, _subscription, _ack -> - :ok - end) - - stub(MockEventStore, :child_spec, fn _application, _config -> - {:ok, [], %{}} - end) - - stub(MockEventStore, :subscribe_to, fn - _event_store_meta, _stream, _handler_name, subscriber, _subscribe_from, _opts -> - send(subscriber, {:subscribed, self()}) - - {:ok, self()} - end) - - :ok + MockEventStoreHelpers.stub_common_event_store() end end diff --git a/test/support/mock_event_store_helpers.ex b/test/support/mock_event_store_helpers.ex new file mode 100644 index 00000000..b067986b --- /dev/null +++ b/test/support/mock_event_store_helpers.ex @@ -0,0 +1,269 @@ +defmodule Commanded.TestSupport.MockEventStoreHelpers do + @moduledoc """ + Named helpers for MockEventStore expectations. + + Replaces inline `expect`/`stub` calls with functions that describe the scenario + under test. Each helper documents when and why to use it. + """ + + import Mox + + alias Commanded.EventStore.Adapters.Mock, as: MockEventStore + alias Commanded.EventStore.RecordedEvent + alias Commanded.UUID + + @doc """ + Apply the common MockEventStore stubs shared by MockEventStoreCase and + MockProjectionCase. Centralizes adapter callback stubs so future changes + are made in one place. + """ + def stub_common_event_store do + stub(MockEventStore, :ack_event, fn _meta, _pid, _event -> :ok end) + + stub(MockEventStore, :child_spec, fn _app, _cfg -> + {:ok, [], %{}} + end) + + stub(MockEventStore, :subscribe_to, fn _meta, _stream, _name, handler, _from, _opts -> + send(handler, {:subscribed, self()}) + {:ok, self()} + end) + + stub(MockEventStore, :subscribe, fn _meta, _stream -> :ok end) + + stub(MockEventStore, :append_to_stream, fn _meta, _uuid, _ver, _events, _opts -> + :ok + end) + + stub(MockEventStore, :stream_forward, fn _meta, _uuid, _from, _batch -> + {:error, :stream_not_found} + end) + + stub(MockEventStore, :read_snapshot, fn _meta, _uuid -> + {:error, :snapshot_not_found} + end) + + stub(MockEventStore, :record_snapshot, fn _meta, _snapshot -> :ok end) + stub(MockEventStore, :delete_snapshot, fn _meta, _uuid -> :ok end) + stub(MockEventStore, :unsubscribe, fn _meta, _sub -> :ok end) + stub(MockEventStore, :delete_subscription, fn _meta, _stream, _name -> :ok end) + + :ok + end + + @doc """ + Use for projection tests that send events directly via `send(projector, {:events, events})`. + Returns `{:ok, self()}` without sending `{:subscribed, self()}` to the handler. + This matches the original projection test setup and avoids Ecto Sandbox connection + ownership issues when the handler processes events in a separate process. + """ + def stub_subscribe_to_for_projections do + stub(MockEventStore, :subscribe_to, fn _meta, _stream, _name, _handler, _from, _opts -> + {:ok, self()} + end) + end + + @doc """ + Use when testing event handler `after_start/1` before the subscription is fully + established. The default stub sends `{:subscribed, self()}` to the handler, + which triggers subscription flow — this override returns `{:ok, handler}` so + you can manually control when (or if) the handler receives the subscribed signal. + """ + def stub_subscribe_to_return_handler do + stub(MockEventStore, :subscribe_to, fn + _event_store, :all, _handler_name, handler, _subscribe_from, _opts -> + {:ok, handler} + end) + end + + @doc """ + Use when simulating a concurrency conflict: another process wrote to the stream + before this append. The aggregate will retry (reload + append again) or give up. + Pass `count:` when the aggregate retries multiple times. + """ + def expect_append_wrong_expected_version(stream_uuid, opts \\ []) do + count = Keyword.get(opts, :count, 1) + + expect(MockEventStore, :append_to_stream, count, fn _meta, + ^stream_uuid, + _exp_ver, + _events, + _opts -> + {:error, :wrong_expected_version} + end) + end + + @doc """ + Use when the aggregate should persist events without conflict. Composes with + `expect_stream_empty/2` for the typical happy path. + """ + def expect_append_succeeds(stream_uuid, opts \\ []) do + count = Keyword.get(opts, :count, 1) + + expect(MockEventStore, :append_to_stream, count, fn _meta, + ^stream_uuid, + _exp_ver, + _events, + _opts -> + :ok + end) + end + + @doc """ + Use when the aggregate loads from an empty stream (new aggregate or no events + after a given version). The default stub returns `{:error, :stream_not_found}`; + this returns `[]` so the aggregate treats it as "no events" rather than missing. + """ + def expect_stream_empty(stream_uuid, opts \\ []) do + count = Keyword.get(opts, :count, 1) + + expect(MockEventStore, :stream_forward, count, fn _meta, ^stream_uuid, _from, _batch_size -> + [] + end) + end + + @doc """ + Use when testing load telemetry for a brand-new aggregate. The stream does not + exist yet, so the aggregate gets `stream_not_found` and never calls populate. + """ + def expect_stream_not_found(stream_uuid) do + expect(MockEventStore, :stream_forward, fn _meta, ^stream_uuid, _from, _batch_size -> + {:error, :stream_not_found} + end) + end + + @doc """ + Use when the aggregate should load existing events (e.g. reload after restart). + Pass a list of `RecordedEvent`s — build them with `build_recorded_event/4`. + """ + def expect_stream_with_events(stream_uuid, events, opts \\ []) do + count = Keyword.get(opts, :count, 1) + + expect(MockEventStore, :stream_forward, count, fn _meta, ^stream_uuid, _from, _batch_size -> + events + end) + end + + @doc """ + Use when testing that the aggregate gives up after a concurrency conflict + (e.g. telemetry for `wrong_expected_version`, or `:too_many_attempts`). Append + fails, reload finds nothing new, append fails again — aggregate stops retrying. + """ + def expect_wrong_expected_version_conflict(stream_uuid) do + expect_append_wrong_expected_version(stream_uuid) + expect_stream_empty(stream_uuid, count: 2) + end + + @doc """ + Use when the aggregate should succeed on first try: new stream, no concurrent + writes. Covers the common "create aggregate and append first event" scenario. + """ + def expect_successful_append_with_empty_stream(stream_uuid) do + expect_append_succeeds(stream_uuid) + expect_stream_empty(stream_uuid) + end + + @doc """ + Use when setting up an aggregate that already has one event (e.g. `OpenAccount`). + Stream is empty from version 1 onward; the first append at version 0 succeeds. + Call before `Supervisor.open_aggregate/3` and the first `Aggregate.execute/4`. + """ + def expect_open_aggregate(stream_uuid) do + expect(MockEventStore, :stream_forward, fn _meta, ^stream_uuid, 1, _batch_size -> + [] + end) + + expect(MockEventStore, :append_to_stream, fn _meta, ^stream_uuid, 0, _events, _opts -> + :ok + end) + end + + @doc """ + Use when testing that the aggregate retries and wins after a conflict. First + append fails; reload discovers the concurrent event; second append succeeds. + Pass the event that "another process" wrote — the aggregate will incorporate it. + """ + def expect_concurrency_retry_succeeds(stream_uuid, concurrent_event) do + expect(MockEventStore, :append_to_stream, fn _meta, ^stream_uuid, 1, _events, _opts -> + {:error, :wrong_expected_version} + end) + + expect(MockEventStore, :stream_forward, fn _meta, ^stream_uuid, 2, _batch_size -> + [concurrent_event] + end) + + expect(MockEventStore, :append_to_stream, fn _meta, ^stream_uuid, 2, _events, _opts -> + :ok + end) + end + + @doc """ + Use when testing retry-after-conflict where the aggregate reloads, applies the + discovered event, and appends successfully. First append fails; stream returns + [] then [event]; second append succeeds. Use for telemetry or span tests that + assert on `wrong_expected_version` count when retry wins. + """ + def expect_wrong_expected_version_retry_succeeds(stream_uuid, event, opts \\ []) do + reload_version = Keyword.get(opts, :reload_version, 1) + + expect(MockEventStore, :append_to_stream, 2, fn + _meta, ^stream_uuid, 0, _events, _opts -> + {:error, :wrong_expected_version} + + _meta, ^stream_uuid, ^reload_version, _events, _opts -> + :ok + end) + + stream_forward_calls = :counters.new(1, []) + + expect(MockEventStore, :stream_forward, 2, fn _meta, + ^stream_uuid, + ^reload_version, + _batch_size -> + n = :counters.get(stream_forward_calls, 1) + :counters.add(stream_forward_calls, 1, 1) + + if n == 0 do + [] + else + [event] + end + end) + end + + @doc """ + Use when testing that the aggregate stops after exhausting retries. Append + keeps failing, reload finds nothing — aggregate returns `:too_many_attempts`. + Pass `count:` to match the aggregate's `retry_attempts` (default 6). + """ + def expect_too_many_retry_attempts(stream_uuid, opts \\ []) do + count = Keyword.get(opts, :count, 6) + expect_append_wrong_expected_version(stream_uuid, count: count) + expect_stream_empty(stream_uuid, count: count) + end + + @doc """ + Build a `RecordedEvent` for `expect_stream_with_events/3` or + `expect_concurrency_retry_succeeds/2`. Pass stream id, event number, and the + domain event struct; use `event_type:` when the inferred type is wrong. + """ + def build_recorded_event(stream_uuid, event_number, data, opts \\ []) do + defaults = [ + event_id: UUID.uuid4(), + event_number: event_number, + stream_id: stream_uuid, + stream_version: event_number, + correlation_id: nil, + causation_id: nil, + event_type: infer_event_type(data), + data: data, + metadata: %{}, + created_at: DateTime.utc_now() + ] + + struct!(RecordedEvent, Keyword.merge(defaults, opts)) + end + + defp infer_event_type(%{__struct__: struct}), do: to_string(struct) + defp infer_event_type(_), do: "Elixir.Commanded.EventStore.RecordedEvent" +end diff --git a/test/support/mock_projection_case.ex b/test/support/mock_projection_case.ex new file mode 100644 index 00000000..0e0bf209 --- /dev/null +++ b/test/support/mock_projection_case.ex @@ -0,0 +1,36 @@ +defmodule Commanded.MockProjectionCase do + @moduledoc false + + use ExUnit.CaseTemplate + + import Mox + + alias Commanded.EventStore.Adapters.Mock, as: MockEventStore + alias Commanded.Projections.Repo + alias Commanded.TestSupport.MockEventStoreHelpers + alias Ecto.Adapters.SQL.Sandbox + + using do + quote do + import Mox + import Commanded.TestSupport.MockEventStoreHelpers + + alias Commanded.EventStore.Adapters.Mock, as: MockEventStore + end + end + + setup [:set_mox_global, :stub_event_store, :verify_on_exit!] + + setup do + start_supervised!({TestApplication, event_store: [adapter: MockEventStore]}) + Sandbox.checkout(Repo) + + :ok + end + + def stub_event_store(_context) do + MockEventStoreHelpers.stub_common_event_store() + MockEventStoreHelpers.stub_subscribe_to_for_projections() + :ok + end +end diff --git a/test/support/mocks.ex b/test/support/mocks.ex index ad36fa51..bc1a7c79 100644 --- a/test/support/mocks.ex +++ b/test/support/mocks.ex @@ -1,4 +1 @@ Mox.defmock(Commanded.EventStore.Adapters.Mock, for: Commanded.EventStore.Adapter) -Mox.defmock(Commanded.Commands.MockRouter, for: Commanded.Commands.Router) -Mox.defmock(Commanded.Application.Mock, for: Commanded.Application) -Mox.defmock(Commanded.Serialization.TypeProvider.Mock, for: Commanded.EventStore.TypeProvider) diff --git a/test/support/runtime_config_projector.ex b/test/support/runtime_config_projector.ex index a882e5d4..c0ffd628 100644 --- a/test/support/runtime_config_projector.ex +++ b/test/support/runtime_config_projector.ex @@ -11,4 +11,9 @@ defmodule Commanded.Projections.RuntimeConfigProjector do Ecto.Multi.insert(multi, :my_projection, %Projection{name: name}) end) + + def after_update(%AnEvent{name: name, pid: pid}, _metadata, _changes) do + send(pid, {:projected, name}) + :ok + end end