"""Lambda test module.""" import json from os import path from unittest.mock import call from unittest.mock import patch import pytest import config from src import app from src.utils.constants import StatementAttachmentFailureReason from src.utils.constants import StatementAttachmentStatus from src.utils.constants import StatementAttachmentType from src.utils.dataclasses import StatementAttachmentPayload from src.utils.error_handling import ClientException from src.utils.error_handling import LambdaException from src.utils.error_handling import TaxDetailsException from tests.conftest import ACCOUNT_ID from tests.conftest import CONTRACT_ID from tests.conftest import STATEMENT_PERIOD_ID MISSING_TAX = StatementAttachmentFailureReason.MISSING_TAX SYSTEM_ERROR = StatementAttachmentFailureReason.SYSTEM_ERROR @patch('src.app.build_distribution_fee_document') def test_generate_attachment_file_distribution_fee( build_distribution_fee_document_mock, request_engine, account_payment_term_fixture, account_fixture, signing_entity_fixture, tax_details_fixture, ): """Test generating a distribution fee invoice file.""" mock_attachment = { 'account_id': ACCOUNT_ID, 'contract_id': CONTRACT_ID, 'statement_period_id': STATEMENT_PERIOD_ID, 'invoice_number': '1234_4321', 'statement_attachment_type': StatementAttachmentType.DISTRIBUTION_FEE, } build_distribution_fee_document_mock.return_value = '1234_4321.pdf' result = app._generate_attachment_file(mock_attachment) assert result == '1234_4321.pdf' build_distribution_fee_document_mock.assert_called_once() @patch('src.app.build_legacy_revenue_report_full') def test_generate_attachment_file_legacy_revenue_report_full(build_legacy_mock): """Test generating legacy revenue full report file.""" mock_attachment = { 'account_id': ACCOUNT_ID, 'subaccount_id': None, 'statement_period_id': STATEMENT_PERIOD_ID, 'statement_period_ids': str(STATEMENT_PERIOD_ID), 'file_type': 'xls', 'statement_attachment_type': StatementAttachmentType.LEGACY_REVENUE_DETAIL_FULL, 'number_format': 'eu', } build_legacy_mock.return_value = 'legacy_report.xls' result = app._generate_attachment_file(mock_attachment) assert result == 'legacy_report.xls' build_legacy_mock.assert_called_once_with( ACCOUNT_ID, str(STATEMENT_PERIOD_ID), None, 'xls', 'eu', None ) @patch('src.app.build_legacy_revenue_report_full') def test_generate_attachment_file_legacy_revenue_report_subaccount(build_legacy_mock): """Test generating legacy revenue report for a subaccount.""" mock_attachment = { 'account_id': ACCOUNT_ID, 'subaccount_id': 54321, 'statement_period_id': STATEMENT_PERIOD_ID, 'statement_period_ids': str(STATEMENT_PERIOD_ID), 'file_type': 'xls', 'statement_attachment_type': StatementAttachmentType.LEGACY_REVENUE_DETAIL_FULL, 'number_format': 'eu', } build_legacy_mock.return_value = 'legacy_report.xls' result = app._generate_attachment_file(mock_attachment) assert result == 'legacy_report.xls' build_legacy_mock.assert_called_once_with( ACCOUNT_ID, str(STATEMENT_PERIOD_ID), 54321, 'xls', 'eu', None ) @patch('src.app.build_legacy_revenue_report_physical') def test_generate_attachment_file_legacy_revenue_report_physical(build_legacy_mock): """Test generating legacy revenue physical report file.""" mock_attachment = { 'account_id': ACCOUNT_ID, 'subaccount_id': None, 'statement_period_id': STATEMENT_PERIOD_ID, 'statement_period_ids': str(STATEMENT_PERIOD_ID), 'file_type': 'xls', 'statement_attachment_type': StatementAttachmentType.LEGACY_REVENUE_DETAIL_PHYSICAL, 'number_format': 'eu', } build_legacy_mock.return_value = 'legacy_report.xls' result = app._generate_attachment_file(mock_attachment) assert result == 'legacy_report.xls' build_legacy_mock.assert_called_once_with( ACCOUNT_ID, str(STATEMENT_PERIOD_ID), None, 'xls', 'eu', None ) @patch('src.app.build_legacy_revenue_report_physical') def test_generate_attachment_file_legacy_revenue_report_physical_subaccount(build_legacy_mock): """Test generating legacy revenue physical report file for a subaccount.""" subaccount_id = 54321 mock_attachment = { 'account_id': ACCOUNT_ID, 'subaccount_id': subaccount_id, 'statement_period_id': STATEMENT_PERIOD_ID, 'statement_period_ids': str(STATEMENT_PERIOD_ID), 'file_type': 'xls', 'statement_attachment_type': StatementAttachmentType.LEGACY_REVENUE_DETAIL_PHYSICAL, 'number_format': 'eu', } build_legacy_mock.return_value = 'legacy_report.xls' result = app._generate_attachment_file(mock_attachment) assert result == 'legacy_report.xls' build_legacy_mock.assert_called_once_with( ACCOUNT_ID, str(STATEMENT_PERIOD_ID), subaccount_id, 'xls', 'eu', None ) @patch('src.app.build_legacy_revenue_report_full') def test_generate_attachment_file_legacy_revenue_report_full_with_filters(build_legacy_mock): """Test generating legacy revenue full report with transaction type filters.""" filters = {'transaction_type_ids': [1, 2, 3], 'variant': 'digital'} mock_attachment = { 'account_id': ACCOUNT_ID, 'subaccount_id': None, 'statement_period_id': STATEMENT_PERIOD_ID, 'statement_period_ids': str(STATEMENT_PERIOD_ID), 'file_type': 'xls', 'statement_attachment_type': StatementAttachmentType.LEGACY_REVENUE_DETAIL_FULL, 'number_format': 'eu', 'filters': filters, } build_legacy_mock.return_value = 'legacy_report_filtered.xls' result = app._generate_attachment_file(mock_attachment) assert result == 'legacy_report_filtered.xls' build_legacy_mock.assert_called_once_with( ACCOUNT_ID, str(STATEMENT_PERIOD_ID), None, 'xls', 'eu', filters ) @patch('src.app.build_legacy_revenue_report_physical') def test_generate_attachment_file_legacy_revenue_report_physical_with_filters(build_legacy_mock): """Test generating legacy revenue physical report with exclude filters.""" filters = {'exclude_transaction_type_ids': [10, 11, 12], 'variant': 'physical'} mock_attachment = { 'account_id': ACCOUNT_ID, 'subaccount_id': None, 'statement_period_id': STATEMENT_PERIOD_ID, 'statement_period_ids': str(STATEMENT_PERIOD_ID), 'file_type': 'txt', 'statement_attachment_type': StatementAttachmentType.LEGACY_REVENUE_DETAIL_PHYSICAL, 'number_format': 'us', 'filters': filters, } build_legacy_mock.return_value = 'legacy_physical_filtered.txt' result = app._generate_attachment_file(mock_attachment) assert result == 'legacy_physical_filtered.txt' build_legacy_mock.assert_called_once_with( ACCOUNT_ID, str(STATEMENT_PERIOD_ID), None, 'txt', 'us', filters ) @patch('src.app.build_neighbouring_rights_label_revenue') def test_generate_attachment_file_neighbouring_rights_label_revenue( build_neighbouring_rights_label_revenue_mock, ): """Test generating a neighbouring rights label revenue file.""" mock_attachment = { 'account_id': ACCOUNT_ID, 'contract_id': CONTRACT_ID, 'statement_period_id': STATEMENT_PERIOD_ID, 'invoice_number': '1234_4321', 'statement_attachment_type': StatementAttachmentType.NEIGHBOURING_RIGHTS_LABEL_REVENUE, } # noqa: E501 build_neighbouring_rights_label_revenue_mock.return_value = 'file.csv' result = app._generate_attachment_file(mock_attachment) assert result == 'file.csv' build_neighbouring_rights_label_revenue_mock.assert_called_once_with( ACCOUNT_ID, CONTRACT_ID, STATEMENT_PERIOD_ID ) @patch('src.app.build_neighbouring_rights_performer_revenue') def test_generate_attachment_file_neighbouring_rights_performer_revenue( build_neighbouring_rights_performer_revenue_mock, ): """Test generating a neighbouring rights performer revenue file.""" mock_attachment = { 'account_id': ACCOUNT_ID, 'contract_id': CONTRACT_ID, 'statement_period_id': STATEMENT_PERIOD_ID, 'invoice_number': '1234_4321', 'statement_attachment_type': StatementAttachmentType.NEIGHBOURING_RIGHTS_PERFORMER_REVENUE, } # noqa: E501 build_neighbouring_rights_performer_revenue_mock.return_value = 'file.csv' result = app._generate_attachment_file(mock_attachment) assert result == 'file.csv' build_neighbouring_rights_performer_revenue_mock.assert_called_once_with( ACCOUNT_ID, CONTRACT_ID, STATEMENT_PERIOD_ID ) @patch('src.app.build_revenue_detail_report') def test_generate_attachment_file_revenue_detail(build_revenue_detail_report_mock): """Test generating a revenue detail report file.""" file_type = 'csv' number_format = 'eu' mock_attachment = { 'account_id': ACCOUNT_ID, 'contract_id': CONTRACT_ID, 'statement_period_id': STATEMENT_PERIOD_ID, 'statement_attachment_type': StatementAttachmentType.REVENUE_DETAIL, 'subaccount_id': None, 'file_type': file_type, 'number_format': number_format, } build_revenue_detail_report_mock.return_value = 'file.csv' result = app._generate_attachment_file(mock_attachment) assert result == 'file.csv' build_revenue_detail_report_mock.assert_called_once_with( ACCOUNT_ID, CONTRACT_ID, STATEMENT_PERIOD_ID, None, file_type, number_format, # noqa: E501 ) @patch('src.app.build_revenue_detail_report') def test_generate_attachment_file_revenue_detail_subaccount(build_revenue_detail_report_mock): """Test generating a revenue detail report file for a subaccount.""" subaccount_id = 43231 file_type = 'csv' number_format = 'eu' mock_attachment = { 'account_id': ACCOUNT_ID, 'contract_id': CONTRACT_ID, 'statement_period_id': STATEMENT_PERIOD_ID, 'statement_attachment_type': StatementAttachmentType.REVENUE_DETAIL, 'subaccount_id': subaccount_id, 'file_type': file_type, 'number_format': number_format, } build_revenue_detail_report_mock.return_value = 'file.csv' result = app._generate_attachment_file(mock_attachment) assert result == 'file.csv' build_revenue_detail_report_mock.assert_called_once_with( ACCOUNT_ID, CONTRACT_ID, STATEMENT_PERIOD_ID, subaccount_id, file_type, number_format, # noqa: E501 ) @patch('src.app.build_self_billing_document') def test_generate_attachment_file_self_billing(build_self_billing_document_mock): """Test generating a self billing invoice file.""" mock_attachment = { 'account_id': ACCOUNT_ID, 'contract_id': CONTRACT_ID, 'statement_period_id': STATEMENT_PERIOD_ID, 'invoice_number': '1234_4321', 'statement_attachment_type': StatementAttachmentType.SELF_BILLING, } build_self_billing_document_mock.return_value = '1234_4321.pdf' result = app._generate_attachment_file(mock_attachment) assert result == '1234_4321.pdf' build_self_billing_document_mock.assert_called_once() def test_generate_attachment_file_unhandled_type(): """Test generating an attachment file with an unhandled type.""" mock_attachment = {'statement_attachment_type': 'somerandomtype'} expected = 'Unhandled statement attachment type: somerandomtype' with pytest.raises(LambdaException) as exception_info: app._generate_attachment_file(mock_attachment) assert exception_info.value.message == expected @patch('src.app._generate_attachment_file') @patch('src.app.OwsMoneyhub.get_statement_attachment') @patch('src.app.os') @patch('src.app.s3') @patch('src.app.OwsMoneyhub.update_statement_attachment') def test_generate_attachment_by_id( update_statement_attachment_mock, s3_mock, os_mock, get_statement_attachment_mock, generate_attachment_file_mock, statement_attachments_fixture, ): """Test generating an attachment fetched by ID.""" statement_attachment_id = 1234 local_file = path.join(config.FILE_OUTPUT_PATH, 'two.pdf') payload = StatementAttachmentPayload( file_location='s3://two.pdf', statement_attachment_status=StatementAttachmentStatus.COMPLETE ) get_statement_attachment_mock.return_value = statement_attachments_fixture[1] generate_attachment_file_mock.return_value = local_file s3_mock.full_path.return_value = 's3://two.pdf' app._generate_attachment_by_id(statement_attachment_id) generate_attachment_file_mock.assert_called_once_with(statement_attachments_fixture[1]) s3_mock.upload_file.assert_called_once_with(local_file, '125/270/2/two.pdf') update_statement_attachment_mock.assert_called_once_with(2, payload) os_mock.remove.assert_called_once_with(local_file) @patch('src.app._generate_attachment_file') @patch('src.app.OwsMoneyhub.get_statement_attachments') @patch('src.app.os') @patch('src.app.s3') @patch('src.app.OwsMoneyhub.update_statement_attachment') def test_generate_attachments_by_account( update_statement_attachment_mock, s3_mock, os_mock, get_statement_attachments_mock, generate_attachment_file_mock, statement_attachments_fixture, ): """Test generating attachments.""" mock_files = [ path.join(config.FILE_OUTPUT_PATH, 'one.pdf'), path.join(config.FILE_OUTPUT_PATH, 'three.pdf'), ] get_statement_attachments_mock.return_value = statement_attachments_fixture generate_attachment_file_mock.side_effect = mock_files s3_mock.full_path.side_effect = ['s3://one.pdf', 's3://three.pdf'] payload_one = StatementAttachmentPayload( file_location='s3://one.pdf', statement_attachment_status=StatementAttachmentStatus.COMPLETE ) payload_two = StatementAttachmentPayload( file_location='s3://three.pdf', statement_attachment_status=StatementAttachmentStatus.COMPLETE, ) app._generate_attachments_by_account(ACCOUNT_ID, STATEMENT_PERIOD_ID, None) generate_attachment_file_mock.assert_has_calls( [ call(statement_attachments_fixture[0]), call(statement_attachments_fixture[2]), ] ) s3_mock.upload_file.assert_has_calls( [ call(mock_files[0], '125/270/1/one.pdf'), call(mock_files[1], '125/270/3/three.pdf'), ] ) update_statement_attachment_mock.assert_has_calls( [ call(1, payload_one), call(3, payload_two), ] ) os_mock.remove.assert_has_calls( [ call(mock_files[0]), call(mock_files[1]), ] ) @patch('src.app._generate_attachment_file') @patch('src.app.OwsMoneyhub.get_statement_attachments') @patch('src.app.os') @patch('src.app.s3') @patch('src.app.OwsMoneyhub.update_statement_attachment') def test_generate_attachments_by_account_error( update_statement_attachment_mock, s3_mock, os_mock, get_statement_attachments_mock, generate_attachment_file_mock, statement_attachments_fixture, ): """Test generating attachments when an error occurs.""" mock_error = LambdaException('Generic error') get_statement_attachments_mock.return_value = statement_attachments_fixture generate_attachment_file_mock.side_effect = ['test', mock_error] s3_mock.full_path.side_effect = ['s3://one.pdf', 's3://three.pdf'] payload_one = StatementAttachmentPayload( file_location='s3://one.pdf', statement_attachment_status=StatementAttachmentStatus.COMPLETE, failure_reason=None, ) payload_two = StatementAttachmentPayload( file_location=None, statement_attachment_status=StatementAttachmentStatus.ERROR, failure_reason=StatementAttachmentFailureReason.SYSTEM_ERROR, ) with pytest.raises(LambdaException) as exception_info: app._generate_attachments_by_account(ACCOUNT_ID, STATEMENT_PERIOD_ID, None) assert exception_info.value.message == mock_error.message assert generate_attachment_file_mock.call_count == 2 assert os_mock.remove.call_count == 1 update_statement_attachment_mock.assert_has_calls( [ call(1, payload_one), call(3, payload_two), ] ) @patch('src.app._generate_attachments_by_account') def test_handler_account(generate_attachments_mock): """Test calling the handler function with an account.""" event = {'account_id': ACCOUNT_ID, 'statement_period_id': STATEMENT_PERIOD_ID} app.handler(event, None) generate_attachments_mock.assert_called_with(ACCOUNT_ID, STATEMENT_PERIOD_ID, None) @patch('src.app._generate_attachment_by_id') @patch('src.app._generate_attachments_by_account') def test_handler_attachment_id( generate_attachments_by_account_mock, generate_attachment_by_id_mock ): """Test calling the handler function with an attachment payload.""" statement_attachment_id = 1234 event = {'statement_attachment_id': statement_attachment_id} app.handler(event, None) generate_attachment_by_id_mock.assert_called_with(statement_attachment_id) generate_attachments_by_account_mock.assert_not_called() @patch('src.app._generate_attachments_by_account') def test_handler_subaccount(generate_attachments_mock): """Test calling the handler function with a payload containing a subaccount ID.""" subaccount_id = 54321 event = { 'account_id': ACCOUNT_ID, 'statement_period_id': STATEMENT_PERIOD_ID, 'subaccount_id': subaccount_id, } app.handler(event, None) generate_attachments_mock.assert_called_with(ACCOUNT_ID, STATEMENT_PERIOD_ID, subaccount_id) @patch('src.app.capture_exception') @patch('src.app._generate_attachments_by_account') def test_handler_error(generate_attachments_mock, capture_exception_mock): """Test calling the handler function with a regular payload.""" mock_error = Exception('Something bad lol') event = {'account_id': ACCOUNT_ID, 'statement_period_id': STATEMENT_PERIOD_ID} generate_attachments_mock.side_effect = mock_error with pytest.raises(Exception) as exception_info: app.handler(event, None) assert exception_info.value == mock_error capture_exception_mock.assert_called_once_with(mock_error) @patch('src.app.capture_exception') @patch('src.app._generate_attachments_by_account') def test_handler_missing_account_id(generate_attachments_mock, capture_exception_mock): """Test calling the handler function when missing an account ID.""" event = {'statement_period_id': STATEMENT_PERIOD_ID} with pytest.raises(ClientException) as exception_info: app.handler(event, None) assert str(exception_info.value) == 'Missing account_id in event payload' generate_attachments_mock.assert_not_called() @patch('src.app.capture_exception') @patch('src.app._generate_attachments_by_account') def test_handler_missing_statement_period_id(generate_attachments_mock, capture_exception_mock): """Test calling the handler function when missing an account ID.""" event = {'account_id': ACCOUNT_ID} with pytest.raises(ClientException) as exception_info: app.handler(event, None) assert str(exception_info.value) == 'Missing statement_period_id in event payload' generate_attachments_mock.assert_not_called() @patch('src.app._generate_attachments_by_account') def test_handler_accounts_records(generate_attachments_mock): """Test calling the handler function with several account payloads.""" event1 = {'account_id': ACCOUNT_ID, 'statement_period_id': STATEMENT_PERIOD_ID} event2 = {'account_id': 12345, 'statement_period_id': 54321, 'subaccount_id': 99999} payload = {'Records': [{'body': json.dumps(event1)}, {'body': json.dumps(event2)}]} app.handler(payload, None) generate_attachments_mock.assert_has_calls( [ call(ACCOUNT_ID, STATEMENT_PERIOD_ID, None), call(12345, 54321, 99999), ] ) @patch('src.app._generate_attachment_by_id') def test_handler_attachments_records(generate_attachment_by_id_mock): """Test calling the handler function with several attachment payloads.""" event1 = {'statement_attachment_id': 1} event2 = {'statement_attachment_id': 2} payload = {'Records': [{'body': json.dumps(event1)}, {'body': json.dumps(event2)}]} app.handler(payload, None) generate_attachment_by_id_mock.assert_has_calls( [ call(1), call(2), ] ) @patch('src.app._generate_attachment_file') @patch('src.app.OwsMoneyhub.get_statement_attachments') @patch('src.app.os') @patch('src.app.s3') @patch('src.app.OwsMoneyhub.update_statement_attachment') def test_generate_attachments_by_account_tax_details_error( update_statement_attachment_mock, s3_mock, os_mock, get_statement_attachments_mock, generate_attachment_file_mock, statement_attachments_fixture, ): """Test generating attachments when an OwsServiceException error occurs.""" mock_error = TaxDetailsException('Missing Tax information') get_statement_attachments_mock.return_value = [statement_attachments_fixture[0]] generate_attachment_file_mock.side_effect = mock_error s3_mock.full_path.side_effect = 's3://one.pdf' payload = StatementAttachmentPayload( statement_attachment_status=StatementAttachmentStatus.ERROR, failure_reason=MISSING_TAX ) with pytest.raises(TaxDetailsException) as exception_info: app._generate_attachments_by_account(ACCOUNT_ID, STATEMENT_PERIOD_ID, None) assert exception_info.value.message == mock_error.message assert exception_info.value.failure_reason == MISSING_TAX update_statement_attachment_mock.assert_called_once_with(1, payload)