@@ -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