Skip to content

Commit 44e2045

Browse files
committed
refactor(trogon_commanded): streamline command handler assertions and identity transformations
- Consolidated event and state assertions into a common function to reduce code duplication and improve clarity. - Enhanced identity transformation logic to handle both events and states uniformly, ensuring accurate testing across different scenarios. - Updated error handling to provide clearer feedback when assertions fail, improving the overall reliability of the test suite. Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
1 parent d900355 commit 44e2045

File tree

1 file changed

+79
-151
lines changed

1 file changed

+79
-151
lines changed

apps/trogon_commanded/lib/trogon/commanded/test_support/command_handler_case.ex

Lines changed: 79 additions & 151 deletions
Original file line numberDiff line numberDiff line change
@@ -108,97 +108,55 @@ defmodule Trogon.Commanded.TestSupport.CommandHandlerCase do
108108
end
109109
end
110110

111-
def assert_events(
112-
initial_events,
113-
command,
114-
expected_events,
115-
aggregate_module,
116-
command_handler_module
117-
) do
118-
result = run_aggregate_with_identity(
119-
initial_events,
120-
command,
121-
aggregate_module,
122-
command_handler_module
123-
)
124-
125-
case result do
111+
def assert_events(initial_events, command, expected_events, aggregate_module, command_handler_module) do
112+
run_and_assert(initial_events, command, aggregate_module, command_handler_module, fn
126113
{_state, actual_events, aggregate_uuid} ->
127-
# Transform expected events to use the correct stream ID
128-
transformed_expected_events = transform_event_identities(expected_events, aggregate_uuid, aggregate_module)
129-
130-
transformed_expected_events = List.wrap(transformed_expected_events)
114+
expected = transform_identities(expected_events, aggregate_uuid, aggregate_module)
115+
assert actual_events == List.wrap(expected)
131116

132-
assert actual_events == transformed_expected_events
133-
134-
{:error, _reason} ->
135-
flunk("Expected success but got error: #{inspect(result)}")
136-
end
117+
{:error, reason} ->
118+
flunk("Expected success but got error: #{inspect(reason)}")
119+
end)
137120
end
138121

139-
def assert_state(
140-
initial_events,
141-
command,
142-
expected_state,
143-
aggregate_module,
144-
command_handler_module
145-
) do
146-
result = run_aggregate_with_identity(
147-
initial_events,
148-
command,
149-
aggregate_module,
150-
command_handler_module
151-
)
152-
153-
case result do
122+
def assert_state(initial_events, command, expected_state, aggregate_module, command_handler_module) do
123+
run_and_assert(initial_events, command, aggregate_module, command_handler_module, fn
154124
{state, _events, aggregate_uuid} ->
155-
# Transform expected state to use the correct stream ID
156-
transformed_expected_state = transform_aggregate_identity(expected_state, aggregate_uuid, aggregate_module)
157-
158-
assert state == transformed_expected_state
125+
expected = transform_identities(expected_state, aggregate_uuid, aggregate_module)
126+
assert state == expected
159127

160-
{:error, _reason} ->
161-
flunk("Expected success but got error: #{inspect(result)}")
162-
end
128+
{:error, reason} ->
129+
flunk("Expected success but got error: #{inspect(reason)}")
130+
end)
163131
end
164132

165-
def assert_error(
166-
initial_events,
167-
command,
168-
expected_error,
169-
aggregate_module,
170-
command_handler_module
171-
) do
172-
result = run_aggregate_with_identity(
173-
initial_events,
174-
command,
175-
aggregate_module,
176-
command_handler_module
177-
)
133+
def assert_error(initial_events, command, expected_error, aggregate_module, command_handler_module) do
134+
run_and_assert(initial_events, command, aggregate_module, command_handler_module, fn
135+
{:error, reason} -> assert reason == expected_error
136+
other -> flunk("Expected error #{inspect(expected_error)}, but got: #{inspect(other)}")
137+
end)
138+
end
178139

179-
case result do
180-
{:error, reason} ->
181-
assert reason == expected_error
182-
other ->
183-
flunk("Expected error #{inspect(expected_error)}, but got: #{inspect(other)}")
184-
end
140+
# Common assertion runner that eliminates duplication
141+
defp run_and_assert(initial_events, command, aggregate_module, command_handler_module, assertion_fn) do
142+
result = run_aggregate_with_identity(initial_events, command, aggregate_module, command_handler_module)
143+
assertion_fn.(result)
185144
end
186145

187146
# Common function that runs aggregate with proper identity handling for all assertion types
188147
defp run_aggregate_with_identity(
189-
initial_events,
190-
command,
191-
aggregate_module,
192-
command_handler_module
193-
) do
194-
148+
initial_events,
149+
command,
150+
aggregate_module,
151+
command_handler_module
152+
) do
195153
assert is_list(initial_events), "Initial events must be a list of events"
196154
aggregate_uuid = extract_aggregate_identity(command, aggregate_module)
197155

198156
# Transform initial events to use the correct stream ID (if applicable)
199157
transformed_initial_events =
200158
if aggregate_uuid do
201-
transform_event_identities(initial_events, aggregate_uuid, aggregate_module)
159+
transform_identities(initial_events, aggregate_uuid, aggregate_module)
202160
else
203161
initial_events
204162
end
@@ -237,8 +195,8 @@ defmodule Trogon.Commanded.TestSupport.CommandHandlerCase do
237195
# This aggregate has identity configuration - transform the results
238196
case result do
239197
{:ok, state, events} ->
240-
transformed_events = transform_event_identities(events, aggregate_uuid, aggregate_module)
241-
transformed_state = transform_aggregate_identity(state, aggregate_uuid, aggregate_module)
198+
transformed_events = transform_identities(events, aggregate_uuid, aggregate_module)
199+
transformed_state = transform_identities(state, aggregate_uuid, aggregate_module)
242200
{:ok, transformed_state, transformed_events}
243201

244202
other ->
@@ -313,36 +271,34 @@ defmodule Trogon.Commanded.TestSupport.CommandHandlerCase do
313271
end
314272

315273
defp extract_aggregate_identity(command, aggregate_module) do
316-
# Follow the same pattern as CommandRouter:
317-
# 1. Try command module first (for transaction scripts)
318-
# 2. Fall back to aggregate module (for regular aggregates)
319274
command_module = command.__struct__
320275

321-
{identifier, identity_prefix} =
322-
if function_exported?(command_module, :aggregate_identifier, 0) and
323-
function_exported?(command_module, :identity_prefix, 0) do
324-
# Transaction script pattern - use command module's configuration
325-
{command_module.aggregate_identifier(), command_module.identity_prefix()}
326-
else
327-
# Regular aggregate pattern - use aggregate module's configuration
328-
{get_identifier(aggregate_module), get_identity_prefix(aggregate_module)}
329-
end
276+
# Get identity configuration - try command module first, then aggregate module
277+
{identifier, identity_prefix} = get_identity_config(command_module, aggregate_module)
330278

331-
# Create a pipeline like Commanded does in production
332-
pipeline = %Pipeline{
333-
command: command,
334-
identity: identifier,
335-
identity_prefix: identity_prefix
336-
}
337-
338-
# Let ExtractAggregateIdentity middleware handle everything
339-
case ExtractAggregateIdentity.before_dispatch(pipeline) do
279+
# Use Commanded's middleware to extract the UUID
280+
%Pipeline{command: command, identity: identifier, identity_prefix: identity_prefix}
281+
|> ExtractAggregateIdentity.before_dispatch()
282+
|> case do
340283
%Pipeline{assigns: %{aggregate_uuid: uuid}} -> uuid
341-
# No transformation applied
342284
%Pipeline{} -> nil
343285
end
344286
end
345287

288+
# Extract identity configuration with fallback logic
289+
defp get_identity_config(command_module, aggregate_module) do
290+
if has_identity_functions?(command_module) do
291+
{command_module.aggregate_identifier(), command_module.identity_prefix()}
292+
else
293+
{get_identifier(aggregate_module), get_identity_prefix(aggregate_module)}
294+
end
295+
end
296+
297+
defp has_identity_functions?(module) do
298+
function_exported?(module, :aggregate_identifier, 0) and
299+
function_exported?(module, :identity_prefix, 0)
300+
end
301+
346302
defp get_identity_prefix(aggregate_module) do
347303
if function_exported?(aggregate_module, :identity_prefix, 0) do
348304
aggregate_module.identity_prefix()
@@ -352,17 +308,14 @@ defmodule Trogon.Commanded.TestSupport.CommandHandlerCase do
352308
end
353309
end
354310

355-
defp transform_event_identities(events, aggregate_uuid, aggregate_module) do
311+
# Unified identity transformation for both events and state
312+
defp transform_identities(data, aggregate_uuid, aggregate_module) do
356313
identifier = get_identifier(aggregate_module)
357314

358-
events
359-
|> List.wrap()
360-
|> Enum.map(&transform_single_event(&1, identifier, aggregate_uuid))
361-
end
362-
363-
defp transform_aggregate_identity(state, aggregate_uuid, aggregate_module) do
364-
identifier = get_identifier(aggregate_module)
365-
transform_single_event(state, identifier, aggregate_uuid)
315+
case data do
316+
items when is_list(items) -> Enum.map(items, &transform_single_item(&1, identifier, aggregate_uuid))
317+
single_item -> transform_single_item(single_item, identifier, aggregate_uuid)
318+
end
366319
end
367320

368321
defp get_identifier(aggregate_module) do
@@ -375,31 +328,20 @@ defmodule Trogon.Commanded.TestSupport.CommandHandlerCase do
375328
end
376329
end
377330

378-
# Helper function to transform a single event/state with consistent logic
379-
defp transform_single_event(item, identifier, aggregate_uuid) do
380-
if Map.has_key?(item, identifier) do
381-
current_identifier = Map.get(item, identifier)
382-
383-
if should_transform_identifier?(current_identifier, aggregate_uuid) do
384-
Map.put(item, identifier, aggregate_uuid)
385-
else
386-
item
387-
end
331+
# Transform a single item's identifier if needed
332+
defp transform_single_item(item, identifier, aggregate_uuid) do
333+
with true <- identifier != nil and Map.has_key?(item, identifier),
334+
current when is_binary(current) <- Map.get(item, identifier),
335+
true <- current != aggregate_uuid do
336+
Map.put(item, identifier, aggregate_uuid)
388337
else
389-
item
338+
_ -> item
390339
end
391340
end
392341

393-
# Determines if an identifier should be transformed based on type and value
394-
defp should_transform_identifier?(current_identifier, aggregate_uuid) do
395-
current_as_string = safe_to_string(current_identifier)
396-
397-
# Only transform string identifiers that don't already match
398-
is_binary(current_identifier) and current_as_string != aggregate_uuid
399-
end
400-
401342
# Safely converts an identifier to string, handling protocol errors
402343
defp safe_to_string(nil), do: nil
344+
403345
defp safe_to_string(value) do
404346
try do
405347
to_string(value)
@@ -411,41 +353,27 @@ defmodule Trogon.Commanded.TestSupport.CommandHandlerCase do
411353
end
412354

413355
defp validate_initial_events_belong_to_aggregate(initial_events, aggregate_uuid, aggregate_module) do
414-
# Skip validation for simple test fixtures without identity configuration
415-
if aggregate_uuid do
416-
identifier = get_identifier(aggregate_module)
417-
418-
# Only validate if the aggregate also has identity configuration
419-
# This handles cases where commands have identity but aggregates don't (transaction script pattern)
420-
if identifier do
421-
initial_events
422-
|> List.wrap()
423-
|> Enum.with_index()
424-
|> Enum.each(&validate_single_event(&1, identifier, aggregate_uuid))
425-
end
426-
# If aggregate doesn't have identity configuration, skip validation
427-
# This is common in transaction script patterns where only the command has identity
356+
# Only validate when both aggregate_uuid and identifier are available
357+
with true <- aggregate_uuid != nil,
358+
identifier when identifier != nil <- get_identifier(aggregate_module) do
359+
initial_events
360+
|> List.wrap()
361+
|> Enum.with_index()
362+
|> Enum.each(&validate_event_belongs_to_aggregate(&1, identifier, aggregate_uuid))
428363
end
429364
end
430365

431-
# Validates that a single event belongs to the expected aggregate
432-
defp validate_single_event({event, index}, identifier, aggregate_uuid) do
366+
defp validate_event_belongs_to_aggregate({event, index}, identifier, aggregate_uuid) do
433367
event_identifier = Map.get(event, identifier)
434-
event_identifier_string = safe_to_string(event_identifier)
435-
436-
# Only validate strict equality for string identifiers
437-
# For complex identifiers (Protobuf structs), be more lenient
438-
should_validate_strict = is_binary(event_identifier) and is_binary(aggregate_uuid)
439368

440-
if should_validate_strict and event_identifier_string != aggregate_uuid do
369+
# Only validate string identifiers for strict equality
370+
if is_binary(event_identifier) and is_binary(aggregate_uuid) and
371+
safe_to_string(event_identifier) != aggregate_uuid do
441372
flunk("""
442373
Initial event at index #{index} does not belong to the aggregate under test.
443-
444-
Expected aggregate identifier: #{inspect(aggregate_uuid)}
445-
Event identifier: #{inspect(event_identifier_string)}
374+
Expected: #{inspect(aggregate_uuid)}
375+
Got: #{inspect(event_identifier)}
446376
Event: #{inspect(event)}
447-
448-
All initial events must belong to the same aggregate being tested.
449377
""")
450378
end
451379
end

0 commit comments

Comments
 (0)