"""Abacus Event logic tests.""" from unittest.mock import MagicMock, patch import pytest from werkzeug.exceptions import BadRequest, NotFound from abacus_event.constants.constants import ( ABACUS_PAYMENTS_GENERATE_LAMBDA_NAME, ALLOWED_EVENTS_FOR_RETRY, EVENT_LAMBDA_NAMES, EVENT_NAMES, EVENT_TARGET_LAMBDA_NAMES, TARGET_TYPES, UNPROVISIONED_LAMBDAS, ) from abacus_event.logic import abacus_event as logic from abacus_event.schemas.abacus_event import ( AbacusEventByTargetTypeSchema, AbacusEventSchema, ) from abacus_event.utils.ows_error import OwsError from tests.utils.factories import AbacusEventFactory @patch('abacus_event.logic.abacus_event.AbacusEvent') def test_create_abacus_event(mock_model): """Test that abacus event is created successfully.""" abacus_event = AbacusEventFactory.create() params = AbacusEventSchema(exclude=('abacus_event_id',)).dump(abacus_event) logic.create_event(**params) mock_model.create.assert_called_with(**params) @patch('abacus_event.logic.abacus_event.AbacusEvent') @patch('abacus_event.logic.abacus_event.invoke_lambda') @patch('abacus_event.logic.abacus_event.handle_event_actions') def test_create_abacus_event_invokes_lambda_by_target_type( mock_airflow_handler, mock_invoke_lambda, mock_model ): """Test successful creation of abacus_event invokes a lambda using extended mapping.""" abacus_event = AbacusEventFactory.create( event_name=EVENT_NAMES.SEND_PAYMENTS, target_type=TARGET_TYPES.WORKSHEET_PAYMENT_CONTRACT_ADVANCE, ) params = AbacusEventSchema(exclude=('abacus_event_id',)).dump(abacus_event) mock_model.create.return_value = abacus_event mock_invoke_lambda.return_value = None logic.create_event(**params) mock_model.create.assert_called_once_with(**params) mock_invoke_lambda.assert_called_once_with( EVENT_TARGET_LAMBDA_NAMES[ (EVENT_NAMES.SEND_PAYMENTS, TARGET_TYPES.WORKSHEET_PAYMENT_CONTRACT_ADVANCE) ], AbacusEventSchema().dump(abacus_event), ) mock_airflow_handler.assert_not_called() @patch('abacus_event.logic.abacus_event.AbacusEvent') @patch('abacus_event.logic.abacus_event.invoke_lambda') @patch('abacus_event.logic.abacus_event.handle_event_actions') def test_create_abacus_event_invokes_lambda( mock_airflow_handler, mock_invoke_lambda, mock_model ): """Test successful creation of abacus_event invokes a lambda when appropriate.""" abacus_event = AbacusEventFactory.create( event_name=EVENT_NAMES.LEGACY_SYNC_CONTRACT ) params = AbacusEventSchema(exclude=('abacus_event_id',)).dump(abacus_event) mock_model.create.return_value = abacus_event mock_invoke_lambda.return_value = None logic.create_event(**params) mock_model.create.assert_called_once_with(**params) mock_invoke_lambda.assert_called_once_with( EVENT_LAMBDA_NAMES[EVENT_NAMES.LEGACY_SYNC_CONTRACT], AbacusEventSchema().dump(abacus_event), ) mock_airflow_handler.assert_not_called() @patch('abacus_event.logic.abacus_event.AbacusEvent') @patch('abacus_event.logic.abacus_event.invoke_lambda') @patch('abacus_event.logic.abacus_event.handle_event_actions') def test_create_abacus_event_triggers_airflow_dag( mock_airflow_handler, mock_invoke_lambda, mock_model ): """Test successful creation of abacus_event triggers a DAG when appropriate.""" abacus_event = AbacusEventFactory.create() params = AbacusEventSchema(exclude=('abacus_event_id',)).dump(abacus_event) mock_model.create.return_value = abacus_event mock_airflow_handler.return_value = None logic.create_event(**params) mock_model.create.assert_called_once_with(**params) mock_airflow_handler.assert_called_once_with(abacus_event) mock_invoke_lambda.assert_not_called() @patch('abacus_event.logic.abacus_event.AbacusEvent') @patch('abacus_event.logic.abacus_event.invoke_lambda') @patch('abacus_event.logic.abacus_event.handle_event_actions') def test_create_payments_generate_abacus_event_invokes_lambda( mock_airflow_handler, mock_invoke_lambda, mock_model ): """Test successful creation of `payments_generate` abacus_event invokes a lambda when FF is enabled.""" abacus_event = AbacusEventFactory.create(event_name=EVENT_NAMES.PAYMENTS_GENERATE) params = AbacusEventSchema(exclude=('abacus_event_id',)).dump(abacus_event) mock_model.create.return_value = abacus_event mock_invoke_lambda.return_value = None logic.create_event(**params) mock_model.create.assert_called_once_with(**params) mock_invoke_lambda.assert_called_once_with( ABACUS_PAYMENTS_GENERATE_LAMBDA_NAME, AbacusEventSchema().dump(abacus_event) ) mock_airflow_handler.assert_not_called() @patch('abacus_event.logic.abacus_event.AbacusEvent') def test_create_abacus_event_is_duplicate_event(mock_model): """Test an error is raised when specified event already exists.""" # Use ACCOUNTING_PERIOD_CLOSE instead of APPLY_PENDING_ADJUSTMENTS # since apply_pending_adjustments now allows duplicates for retry functionality abacus_event = AbacusEventFactory.create( event_name=EVENT_NAMES.ACCOUNTING_PERIOD_CLOSE, target_type=TARGET_TYPES.ACCOUNTING_PERIOD, target_id=123, ) params = AbacusEventSchema(exclude=('abacus_event_id',)).dump(abacus_event) mock_model.get_events_by_target_type.return_value = [abacus_event] with pytest.raises(BadRequest): logic.create_event(**params) mock_model.create.assert_not_called() @patch('abacus_event.logic.abacus_event.AbacusEvent') @patch('abacus_event.logic.abacus_event.invoke_lambda') @patch('abacus_event.logic.abacus_event.handle_event_actions') @patch('abacus_event.logic.abacus_event.update_rollback_time') def test_create_abacus_event_handles_ows_error( mock_update_rollback_time, mock_airflow_handler, mock_invoke_lambda, mock_model ): """Test create event method rolls back event and raises error on failure.""" abacus_event = AbacusEventFactory.create( event_name=EVENT_NAMES.LEGACY_SYNC_CONTRACT ) params = AbacusEventSchema(exclude=('abacus_event_id',)).dump(abacus_event) mock_model.create.return_value = abacus_event mock_invoke_lambda.side_effect = OwsError(message='Oops', status=400) with pytest.raises(OwsError): logic.create_event(**params) mock_model.create.assert_called_once_with(**params) mock_invoke_lambda.assert_called_once_with( EVENT_LAMBDA_NAMES[EVENT_NAMES.LEGACY_SYNC_CONTRACT], AbacusEventSchema().dump(abacus_event), ) mock_airflow_handler.assert_not_called() mock_update_rollback_time.assert_called_once_with( abacus_event.target_type, abacus_event.target_id, abacus_event.event_name ) @patch('abacus_event.logic.abacus_event.AbacusEvent') def test_get_payment_approval_count(mock_model): """Test get_payment_approval_count method.""" mock_data = {'approval_count': 6, 'payment_group_payment_id': 1} mock_model.get_payment_approval_count.return_value = mock_data res = logic.get_payment_approval_count(mock_data.get('payment_group_payment_id')) assert res == mock_data @patch('abacus_event.logic.abacus_event.AbacusEvent') def test_update_rollback_time(mock_model): """Test update_rollback_time method.""" event_name = 'payment_approval' target_id = 1 target_type = 'payment_group_payment' AbacusEventFactory.create( event_name=event_name, target_id=target_id, target_type=target_type ) logic.update_rollback_time(target_type, target_id, event_name) mock_model.update_rollback_time.assert_called_once_with( target_type, target_id, event_name ) @patch('abacus_event.logic.abacus_event.AbacusEvent') def test_get_events_by_target_type(mock_model): """Test get_events_by_target_type method.""" target_type = 'payment_group_payment' target_id = 1 logic.get_events_by_target_type(target_type, target_id) mock_model.get_events_by_target_type.assert_called_once_with(target_type, target_id) @patch('abacus_event.logic.abacus_event.AbacusEvent') def test_dataload_events_by_target_groups_events_per_id(mock_model): """Test dataload_events_by_target returns one ordered entry per target id.""" events = [ AbacusEventFactory.build( target_id=1, target_type='statement_period_adjustment_file' ), AbacusEventFactory.build( target_id=1, target_type='statement_period_adjustment_file' ), AbacusEventFactory.build( target_id=2, target_type='statement_period_adjustment_file' ), ] mock_model.get_events_by_target_ids.return_value = events dumped = AbacusEventByTargetTypeSchema(many=True).dump(events) result = logic.dataload_events_by_target( 'statement_period_adjustment_file', [1, 2, 3] ) # Ordered per requested id: id 1 has two events, id 2 one, id 3 none (None). assert result.message == { 'items': [ {'data': [dumped[0], dumped[1]]}, {'data': [dumped[2]]}, {'data': None}, ] } mock_model.get_events_by_target_ids.assert_called_once_with( 'statement_period_adjustment_file', [1, 2, 3] ) @patch('abacus_event.logic.abacus_event.AbacusEvent') def test_dataload_events_by_target_no_events_returns_null_entries(mock_model): """Test dataload_events_by_target maps ids with no events to data None.""" mock_model.get_events_by_target_ids.return_value = [] result = logic.dataload_events_by_target('statement_period_adjustment_file', [1, 2]) assert result.message == {'items': [{'data': None}, {'data': None}]} @patch('abacus_event.logic.abacus_event.g') @patch('abacus_event.logic.abacus_event.AbacusEventByTargetTypeSchema') @patch('abacus_event.logic.abacus_event.AbacusEvent') def test_dataload_events_by_target_logs_when_records_lack_target_id( mock_model, mock_schema, mock_g ): """A non-empty fetch whose records carry no target_id is logged loudly.""" mock_model.get_events_by_target_ids.return_value = ['event'] # Simulate a serializer field mismatch: records present, none carry target_id. mock_schema.return_value.dump.return_value = [{'event_name': 'x'}] # Plain sync mock for the leaf: g.log.error is a regular call, not a coroutine. mock_g.log.error = MagicMock() logic.dataload_events_by_target('statement_period_adjustment_file', [1]) mock_g.log.error.assert_called_once() @patch('abacus_event.logic.abacus_event.db.session.execute') @patch('abacus_event.models.abacus_event.AbacusEvent.get_filtered_query') def test_dataload_events_by_ids(mock_query, mock_execute): """Test dataload_events_by_ids function.""" events = AbacusEventFactory.create_batch(3) event_ids = [event.abacus_event_id for event in events] # Mock the select statement mock_stmt = MagicMock() mock_query.return_value = mock_stmt # Mock the execution chain: execute().scalars().all() mock_scalars = MagicMock() mock_scalars.all.return_value = events mock_execute.return_value.scalars.return_value = mock_scalars res = logic.dataload_events_by_ids(event_ids) assert res.message == { 'items': [{'data': AbacusEventSchema().dump(event)} for event in events] } mock_query.assert_called_once_with(event_ids=event_ids) mock_execute.assert_called_once_with(mock_stmt) @patch('abacus_event.logic.abacus_event.AbacusEvent') def test__is_duplicate_event_accounting_period_close(mock_model): """Test whether 'accounting_period_close' event already exists for target.""" abacus_event = AbacusEventFactory.create( event_name=EVENT_NAMES.ACCOUNTING_PERIOD_CLOSE, target_type=TARGET_TYPES.ACCOUNTING_PERIOD, target_id=1, ) mock_model.get_events_by_target_type.return_value = [abacus_event] assert logic._is_duplicate_event( abacus_event.event_name, abacus_event.target_id, abacus_event.target_type ) assert not logic._is_duplicate_event( 'fake_event', 'fake_target_type', 'fake_target_id' ) @patch('abacus_event.logic.abacus_event.AbacusEvent') def test__is_duplicate_event_apply_pending_adjustments(mock_model): """Test that 'apply_pending_adjustments' allows duplicates for retry functionality.""" abacus_event = AbacusEventFactory.create( event_name=EVENT_NAMES.APPLY_PENDING_ADJUSTMENTS, target_type=TARGET_TYPES.STATEMENT_PERIOD_ADJUSTMENT_FILE, target_id=123, ) mock_model.get_events_by_target_type.return_value = [abacus_event] # apply_pending_adjustments should NOT be considered duplicate (returns False) # to allow retry functionality assert not logic._is_duplicate_event( abacus_event.event_name, abacus_event.target_id, abacus_event.target_type ) assert not logic._is_duplicate_event( 'fake_event', 'fake_target_type', 'fake_target_id' ) @patch('abacus_event.logic.abacus_event.AbacusEvent') def test__is_duplicate_event_adjustment_file_upload(mock_model): """Test whether 'adjustment_file_upload' event already exists.""" abacus_event = AbacusEventFactory.create( event_name=EVENT_NAMES.ADJUSTMENT_FILE_UPLOAD, target_type=TARGET_TYPES.STATEMENT_PERIOD_ADJUSTMENT_FILE, target_id=123, ) mock_model.get_events_by_target_type.return_value = [abacus_event] assert logic._is_duplicate_event( abacus_event.event_name, abacus_event.target_id, abacus_event.target_type ) assert not logic._is_duplicate_event( 'fake_event', 'fake_target_type', 'fake_target_id' ) @patch('abacus_event.logic.abacus_event.AbacusEvent') def test__is_duplicate_event_adjustment_file_worksheet_import(mock_model): """Test whether 'adjustment_file_worksheet_import' event already exists.""" abacus_event = AbacusEventFactory.create( event_name=EVENT_NAMES.ADJUSTMENT_FILE_WORKSHEET_IMPORT, target_type=TARGET_TYPES.STATEMENT_PERIOD_ADJUSTMENT_FILE, target_id=123, ) mock_model.get_events_by_target_type.return_value = [abacus_event] assert logic._is_duplicate_event( abacus_event.event_name, abacus_event.target_id, abacus_event.target_type ) assert not logic._is_duplicate_event( 'fake_event', 'fake_target_type', 'fake_target_id' ) @patch('abacus_event.logic.abacus_event.AbacusEvent') @patch('abacus_event.logic.abacus_event.invoke_lambda') @patch('abacus_event.logic.abacus_event.handle_event_actions') def test_create_snapshot_contracts_abacus_event_invokes_lambda( mock_airflow_handler, mock_invoke_lambda, mock_model ): """Test successful creation of snapshot_contracts abacus_event invokes a lambda.""" abacus_event = AbacusEventFactory.create(event_name=EVENT_NAMES.SNAPSHOT_CONTRACTS) params = AbacusEventSchema(exclude=('abacus_event_id',)).dump(abacus_event) mock_model.create.return_value = abacus_event mock_invoke_lambda.return_value = None logic.create_event(**params) mock_model.create.assert_called_once_with(**params) mock_invoke_lambda.assert_called_once_with( EVENT_LAMBDA_NAMES[EVENT_NAMES.SNAPSHOT_CONTRACTS], AbacusEventSchema().dump(abacus_event), ) mock_airflow_handler.assert_not_called() def test__is_retry_allowed_all_allowed(): """Test that all events having names in ALLOWED_EVENTS_FOR_RETRY returns True.""" events = [ AbacusEventFactory.build(event_name=name) for name in ALLOWED_EVENTS_FOR_RETRY ] assert logic._is_retry_allowed(events) is True def test__is_retry_allowed_not_all_allowed(): """Test that presence of any event not in ALLOWED_EVENTS_FOR_RETRY returns False.""" events = [ AbacusEventFactory.build(event_name=list(ALLOWED_EVENTS_FOR_RETRY)[0]), AbacusEventFactory.build(event_name='not_allowed_event'), ] assert logic._is_retry_allowed(events) is False def test__is_retry_allowed_empty_list(): """Test that an empty list of events returns True.""" assert logic._is_retry_allowed([]) is False @pytest.mark.parametrize('is_retry', [True, False]) @pytest.mark.parametrize('criteria,lambda_name', EVENT_TARGET_LAMBDA_NAMES.items()) @patch('abacus_event.logic.abacus_event.invoke_lambda') @patch('abacus_event.logic.abacus_event.handle_event_actions') def test__execute_event_invokes_target_lambda( mock_handle_event_actions, mock_invoke_lambda, criteria, lambda_name, is_retry ): """Executes by event_name + target_type criteria.""" event_name, target_type = criteria event = AbacusEventFactory.create( event_name=event_name, target_type=target_type, ) logic._execute_event(event, is_retry) payload = logic.schema.dump(event) if is_retry: payload['is_retry'] = True mock_invoke_lambda.assert_called_once_with( lambda_name, payload, ) mock_handle_event_actions.assert_not_called() @pytest.mark.parametrize('is_retry', [True, False]) @pytest.mark.parametrize('event_name,lambda_name', EVENT_LAMBDA_NAMES.items()) @patch('abacus_event.logic.abacus_event.invoke_lambda') @patch('abacus_event.logic.abacus_event.handle_event_actions') def test__execute_event_invokes_event_lambda( mock_handle_event_actions, mock_invoke_lambda, event_name, lambda_name, is_retry ): """Executes by event_name criteria.""" event = AbacusEventFactory.create(event_name=event_name) logic._execute_event(event, is_retry) payload = logic.schema.dump(event) if is_retry: payload['is_retry'] = True if lambda_name in UNPROVISIONED_LAMBDAS: mock_invoke_lambda.assert_called_once_with(lambda_name, payload, '$LATEST') else: mock_invoke_lambda.assert_called_once_with(lambda_name, payload) mock_handle_event_actions.assert_not_called() @pytest.mark.parametrize('is_retry', [True, False]) @patch('abacus_event.logic.abacus_event.invoke_lambda') @patch('abacus_event.logic.abacus_event.handle_event_actions') def test__execute_event_invokes_payments_generate_lambda( mock_handle_event_actions, mock_invoke_lambda, is_retry ): """Executes specific case for payments_generate event.""" event = AbacusEventFactory.create(event_name=EVENT_NAMES.PAYMENTS_GENERATE) logic._execute_event(event, is_retry) payload = logic.schema.dump(event) if is_retry: payload['is_retry'] = True mock_invoke_lambda.assert_called_once_with( ABACUS_PAYMENTS_GENERATE_LAMBDA_NAME, payload ) mock_handle_event_actions.assert_not_called() @patch('abacus_event.logic.abacus_event.invoke_lambda') @patch('abacus_event.logic.abacus_event.handle_event_actions') def test__execute_event_triggers_airflow_handler( mock_handle_event_actions, mock_invoke_lambda ): """Executes default case triggering airflow handler.""" event = AbacusEventFactory.create(event_name='some_event_not_in_lambda_mappings') logic._execute_event(event) mock_handle_event_actions.assert_called_once_with(event) mock_invoke_lambda.assert_not_called() @patch('abacus_event.logic.abacus_event.update_rollback_time') @patch('abacus_event.logic.abacus_event.invoke_lambda') @patch('abacus_event.logic.abacus_event.handle_event_actions') def test__execute_event_handles_ows_error( mock_handle_event_actions, mock_invoke_lambda, mock_update_rollback_time ): """Test _execute_event method rolls back event and raises error on failure.""" event = AbacusEventFactory.create(event_name=EVENT_NAMES.LEGACY_SYNC_CONTRACT) mock_invoke_lambda.side_effect = OwsError(message='Oops', status=400) with pytest.raises(OwsError): logic._execute_event(event) mock_update_rollback_time.assert_called_once_with( event.target_type, event.target_id, event.event_name ) @pytest.mark.parametrize('event_name', ALLOWED_EVENTS_FOR_RETRY) @patch('abacus_event.logic.abacus_event.AbacusEvent') @patch('abacus_event.logic.abacus_event._execute_event') def test_retry_events_success(mock_execute_event, mock_model, event_name): """Test retry_events successfully retries allowed events.""" abacus_event = AbacusEventFactory.create( event_name=event_name, target_type='some_type', target_id=1, ) mock_model.get_events_by_target_type.return_value = [abacus_event] result = logic.retry_events('some_type', 1, abacus_event.event_name) mock_model.get_events_by_target_type.assert_called_once_with( 'some_type', 1, abacus_event.event_name ) mock_execute_event.assert_called_once_with(abacus_event, is_retry=True) assert result == AbacusEventSchema(many=True).dump([abacus_event]) @patch('abacus_event.logic.abacus_event.AbacusEvent') def test_retry_events_not_allowed(mock_model): """Test retry_events raises BadRequest if event is not allowed for retry.""" abacus_event = AbacusEventFactory.create( event_name='not_allowed_event', target_type='some_type', target_id=1, ) mock_model.get_events_by_target_type.return_value = [abacus_event] with pytest.raises(BadRequest): logic.retry_events('some_type', 1, abacus_event.event_name) @patch('abacus_event.logic.abacus_event.AbacusEvent') def test_retry_events_no_events(mock_model): """Test retry_events raises BadRequest if no events found.""" mock_model.get_events_by_target_type.return_value = [] with pytest.raises(NotFound): logic.retry_events('some_type', 1, 'any_event')