"""Unit tests for ECS task logic.""" from datetime import datetime from unittest.mock import Mock from unittest.mock import patch from botocore.exceptions import ClientError import pytest from moneyhub.logic.ecs_task import _build_ecs_task_configuration from moneyhub.logic.ecs_task import _build_network_configuration from moneyhub.logic.ecs_task import _build_step_function_input from moneyhub.logic.ecs_task import _get_container_from_task_definition from moneyhub.logic.ecs_task import _get_task_definition from moneyhub.logic.ecs_task import trigger_dbt_refresh_task from moneyhub.schemas.ecs_task import DbtRefreshRequest @patch('moneyhub.logic.ecs_task.Config.DBT_REFRESH_STATE_MACHINE_ARN', 'arn:aws:states:us-east-1:123456789:stateMachine:test-dbt-sm') @patch('moneyhub.logic.ecs_task.boto3') def test_trigger_dbt_refresh_task_success(mock_boto3): """Test successful Step Function triggering with dbt parameters.""" mock_sfn_client = Mock() mock_boto3.client.return_value = mock_sfn_client mock_execution = { 'executionArn': 'arn:aws:states:us-east-1:123456789:execution:test-dbt-sm:exec-id', 'startDate': datetime(2025, 11, 28, 12, 0, 0) } mock_sfn_client.start_execution.return_value = mock_execution request = DbtRefreshRequest( full_refresh=True, select_models='model1 model2' ) result = trigger_dbt_refresh_task(request) assert result['execution_arn'] == mock_execution['executionArn'] assert result['start_date'] == '2025-11-28T12:00:00' mock_sfn_client.start_execution.assert_called_once() @patch('moneyhub.logic.ecs_task.Config.DBT_REFRESH_STATE_MACHINE_ARN', 'arn:aws:states:us-east-1:123456789:stateMachine:test-dbt-sm') @patch('moneyhub.logic.ecs_task.boto3') def test_trigger_dbt_refresh_task_step_function_error(mock_boto3): """Test handling when Step Function execution fails.""" mock_sfn_client = Mock() mock_boto3.client.return_value = mock_sfn_client mock_sfn_client.start_execution.side_effect = ClientError( error_response={ 'Error': { 'Code': 'StateMachineDoesNotExist', 'Message': 'State Machine does not exist' } }, operation_name='StartExecution' ) request = DbtRefreshRequest() with pytest.raises(Exception) as exc_info: trigger_dbt_refresh_task(request) assert 'Failed to trigger dbt refresh task' in str(exc_info.value) @patch('moneyhub.logic.ecs_task.Config.TASK_FAMILY', 'dbt-accounting') def test_get_task_definition_success(): """Test successful task definition retrieval.""" mock_ecs_client = Mock() expected_task_definition = { 'taskDefinitionArn': 'arn:aws:ecs:us-east-1:123456789:task-definition/dbt-accounting:1', 'containerDefinitions': [] } mock_ecs_client.describe_task_definition.return_value = { 'taskDefinition': expected_task_definition } result = _get_task_definition(mock_ecs_client, 'dbt-accounting') assert result == expected_task_definition mock_ecs_client.describe_task_definition.assert_called_once_with( taskDefinition='dbt-accounting' ) @patch('moneyhub.logic.ecs_task.Config.TASK_FAMILY', 'dbt-accounting') def test_get_task_definition_not_found(): """Test task definition not found error handling.""" mock_ecs_client = Mock() mock_ecs_client.describe_task_definition.side_effect = ClientError( error_response={ 'Error': { 'Code': 'ClientException', 'Message': 'Task definition not found' } }, operation_name='DescribeTaskDefinition' ) with pytest.raises(Exception) as exc_info: _get_task_definition(mock_ecs_client, 'dbt-accounting') assert "Task definition 'dbt-accounting' not found" in str(exc_info.value) def test_build_step_function_input_basic(): """Test Step Function input building with basic parameters.""" request = DbtRefreshRequest( full_refresh=True, select_models='model1 model2', exclude_models='old_model' ) result = _build_step_function_input(request) assert result['DBT_FULL_REFRESH'] == 'true' assert result['DBT_SELECT_MODELS'] == 'model1 model2' assert result['DBT_EXCLUDE_MODELS'] == 'old_model' def test_build_step_function_input_with_defaults(): """Test Step Function input with default values.""" request = DbtRefreshRequest( full_refresh=False ) result = _build_step_function_input(request) assert result['DBT_FULL_REFRESH'] == 'false' assert result['DBT_SELECT_MODELS'] == 'ledger_summary_dbt account_statement_periods_dbt combined_expenses_dbt combined_expenses_by_artist_dbt expenses_by_subaccount_dbt combined_advances_dbt combined_adjustments_dbt combined_adjustments_by_type_dbt combined_payments_dbt' # noqa: E501 assert result['DBT_EXCLUDE_MODELS'] == 'abacus_fact_sales_unified_dbt workstation_fact_sales_unified_dbt' # noqa: E501 @patch('moneyhub.logic.ecs_task.Config.CONTAINER_NAME', 'dbt-accounting') def test_get_container_from_task_definition_success(): """Test successful container retrieval from task definition.""" task_definition = { 'containerDefinitions': [ {'name': 'other-container'}, {'name': 'dbt-accounting', 'environment': []}, {'name': 'another-container'} ] } result = _get_container_from_task_definition(task_definition, 'dbt-accounting') assert result['name'] == 'dbt-accounting' @patch('moneyhub.logic.ecs_task.Config.CONTAINER_NAME', 'missing-container') def test_get_container_from_task_definition_not_found(): """Test container not found error handling.""" task_definition = { 'containerDefinitions': [ {'name': 'other-container'}, {'name': 'dbt-accounting'} ] } with pytest.raises(Exception) as exc_info: _get_container_from_task_definition(task_definition, 'missing-container') assert "Container 'missing-container' not found in task definition" in str(exc_info.value) @patch('moneyhub.logic.ecs_task.Config.ECS_TASK_SUBNETS', 'subnet-1,subnet-2') @patch('moneyhub.logic.ecs_task.Config.ECS_TASK_SECURITY_GROUPS', 'sg-1,sg-2') @patch('moneyhub.logic.ecs_task.Config.ECS_TASK_ASSIGN_PUBLIC_IP', 'ENABLED') def test_build_network_configuration_with_values(): """Test network configuration building with values.""" result = _build_network_configuration() assert result['subnets'] == ['subnet-1', 'subnet-2'] assert result['securityGroups'] == ['sg-1', 'sg-2'] assert result['assignPublicIp'] == 'ENABLED' def test_build_ecs_task_configuration_generic(): """Test the generic ECS task configuration builder.""" mock_ecs_client = Mock() mock_task_definition = { 'taskDefinitionArn': 'arn:aws:ecs:us-east-1:123456789:task-definition/my-task:1', 'containerDefinitions': [ { 'name': 'my-container', 'environment': [ {'name': 'EXISTING_VAR', 'value': 'existing_value'} ] } ] } mock_ecs_client.describe_task_definition.return_value = { 'taskDefinition': mock_task_definition } result = _build_ecs_task_configuration( ecs_client=mock_ecs_client, task_family='my-task', container_name='my-container', cluster_name='my-cluster', environment_variables={'NEW_VAR': 'new_value', 'OVERRIDE_VAR': 'override'}, network_config={ 'subnets': ['subnet-123'], 'securityGroups': ['sg-456'], 'assignPublicIp': 'ENABLED' }, tags=[ {'key': 'Service', 'value': 'my-service'}, {'key': 'Environment', 'value': 'test'} ], command=['echo', 'hello'] ) assert result['cluster'] == 'my-cluster' assert result['taskDefinition'] == mock_task_definition['taskDefinitionArn'] assert result['launchType'] == 'FARGATE' def test_build_ecs_task_configuration_task_not_found(): """Test error handling when task definition not found.""" mock_ecs_client = Mock() mock_ecs_client.describe_task_definition.side_effect = ClientError( error_response={ 'Error': { 'Code': 'ClientException', 'Message': 'Task definition not found' } }, operation_name='DescribeTaskDefinition' ) with pytest.raises(Exception) as exc_info: _build_ecs_task_configuration( ecs_client=mock_ecs_client, task_family='missing-task', container_name='my-container', cluster_name='my-cluster', environment_variables={}, network_config={}, tags=[], command=['echo', 'hello'] ) assert "Task definition 'missing-task' not found" in str(exc_info.value)