"""Test for the collaborator reports logic.""" from collections import namedtuple import uuid import flask import pytest from pytest_mock import MockFixture from collaborator.constants import error from collaborator.logic import reports from collaborator.models.ows import ows_moneyhub, ows_users from collaborator.models.rds import report_persister, report_run_persister from collaborator.utils import s3 from collaborator.utils.error import OwsError from collaborator.utils.typing import Account, Resource, Sort from tests.testutils import fixtures, report_fixtures def test_get_reports(mocker, mock_account): """Test getting reports.""" mock_reports = [{"id": 1}, {"id": 2}, {"id": 3}] mock_get_report = mocker.patch.object( report_persister.ReportPersister, "get_items", return_value=mock_reports ) result = reports.get_reports( mock_account, status="ERROR", report_run_uuid="eb25ed08-9b84-415b-8103-abdd0833b2f3", ) mock_get_report.assert_called_with( account=mock_account, sort=None, has_transaction=None, status="ERROR", report_run_uuid="eb25ed08-9b84-415b-8103-abdd0833b2f3", collaborator_id=None, report_id=None, collaborator_dp_enabled=None, ) assert result.message == { "items": mock_reports, "pagination": {"type": "standard", "total_records": len(mock_reports)}, } def test_get_reports_sorted(mocker, mock_account): """Test getting a sorted list of reports.""" mock_reports = [] mock_get_report = mocker.patch.object( report_persister.ReportPersister, "get_items", return_value=mock_reports ) result = reports.get_reports(mock_account, sort_key="test", sort_direction="ASC") mock_get_report.assert_called_with( account=mock_account, sort=Sort("test", "ASC"), has_transaction=None, status=None, report_run_uuid=None, collaborator_id=None, report_id=None, collaborator_dp_enabled=None, ) assert result.message == { "items": mock_reports, "pagination": {"type": "standard", "total_records": len(mock_reports)}, } @pytest.mark.parametrize( "limit, offset, expected", [ (1, 2, [{"id": 3}]), (0, 2, [{"id": 3}, {"id": 4}, {"id": 5}]), (2, 0, [{"id": 1}, {"id": 2}]), (0, 0, [{"id": 1}, {"id": 2}, {"id": 3}, {"id": 4}, {"id": 5}]), ], ) def test_get_reports_paginated(mocker, limit, offset, expected, mock_account): """Test getting a paginated list of reports.""" mock_get_report = mocker.patch.object( report_persister.ReportPersister, "get_items", return_value=[{"id": 1}, {"id": 2}, {"id": 3}, {"id": 4}, {"id": 5}], ) result = reports.get_reports(mock_account, limit=limit, offset=offset) mock_get_report.assert_called_with( account=mock_account, sort=None, has_transaction=None, status=None, report_run_uuid=None, collaborator_id=None, report_id=None, collaborator_dp_enabled=None, ) assert result.message == { "items": expected, "pagination": {"type": "standard", "total_records": 5}, } @pytest.mark.parametrize( "account, collaborator_id", [(Account("subaccount", "1234"), 555), (Account("vendor", "24601"), 777)], ) def test_get_reports_for_collaborator(mocker, account, collaborator_id): """Test getting collaborators reports by a collaborator id.""" mock_reports = [{"id": 1}, {"id": 2}] mock_get_report = mocker.patch.object( report_persister.ReportPersister, "get_items", return_value=mock_reports ) result = reports.get_reports_for_collaborator( collaborator_id=collaborator_id, account=account, status="GENERATED" ) assert result == mock_reports mock_get_report.assert_called_with( account, collaborator_id=collaborator_id, status="GENERATED", has_transaction=None, sort=None, ) @pytest.mark.parametrize( "account, collaborator_id", [(Account("subaccount", "1234"), 555), (Account("vendor", "24601"), 777)], ) def test_get_reports_for_collaborator_with_manual_automatic_types( mocker, account, collaborator_id ): """Test getting collaborators reports by a collaborator id.""" mock_reports = [{"id": 1}, {"id": 2}] mock_get_report = mocker.patch.object( report_persister.ReportPersister, "get_items", return_value=mock_reports ) result = reports.get_reports_for_collaborator( collaborator_id=collaborator_id, account=account ) assert result == mock_reports mock_get_report.assert_called_with( account, collaborator_id=collaborator_id, status=None, has_transaction=None, sort=None, ) @pytest.mark.parametrize("has_transaction", [True, False]) def test_get_reports_for_collaborator_has_transaction( has_transaction, mocker, mock_account ): """Test getting reports.""" collaborator_id = 123 mock_reports = [{"id": 1}, {"id": 2}, {"id": 3}] mock_get_report = mocker.patch.object( report_persister.ReportPersister, "get_items", return_value=mock_reports ) result = reports.get_reports_for_collaborator( collaborator_id, mock_account, has_transaction ) assert result == mock_reports mock_get_report.assert_called_with( mock_account, collaborator_id=collaborator_id, has_transaction=has_transaction, status=None, sort=None, ) @pytest.mark.parametrize( "account, collaborator_id", [(Account("subaccount", "1234"), 555), (Account("vendor", "24601"), 777)], ) def test_get_reports_for_collaborator_paginated(mocker, account, collaborator_id): """Test getting collaborators reports by a collaborator id.""" mock_reports = [{"id": 1}, {"id": 2}, {"id": 3}] mock_get_report = mocker.patch.object( report_persister.ReportPersister, "get_items", return_value=mock_reports ) result = reports.get_reports_for_collaborator( collaborator_id=collaborator_id, account=account, status="GENERATED", sort_key="generated_date", sort_direction="DESC", limit=1, offset=1, ) assert result == [mock_reports[1]] mock_get_report.assert_called_with( account, collaborator_id=collaborator_id, status="GENERATED", sort=Sort(key="generated_date", direction="DESC"), has_transaction=None, ) @pytest.mark.parametrize("has_transaction", [True, False]) def test_get_reports_has_transaction(has_transaction, mocker, mock_account): """Test getting reports.""" mock_reports = [{"id": 1}, {"id": 2}, {"id": 3}] mock_get_report = mocker.patch.object( report_persister.ReportPersister, "get_items", return_value=mock_reports ) result = reports.get_reports(mock_account, has_transaction=has_transaction) assert result.message == { "items": mock_reports, "pagination": {"type": "standard", "total_records": len(mock_reports)}, } mock_get_report.assert_called_with( account=mock_account, has_transaction=has_transaction, sort=None, status=None, report_run_uuid=None, collaborator_id=None, report_id=None, collaborator_dp_enabled=None, ) @pytest.mark.parametrize( [ "first_period_id", "last_period_id", "period_ids", "period_name", "collaborators", "report_run_name", "collaborator_ids", "currency", "number_format", ], [ ( 1, 1, "1", "Jan 1999", [{"id": 42, "splits": 1}], "run 1", None, "USD", "us", ), ( 1, 3, "1,2,3", "Q1 1999", [{"id": 1, "splits": 2}, {"id": 2, "splits": 1}], "run 2", None, "USD", "us", ), ( 4, 8, "4,5,6,7,8", "Apr-Oct 1999", [{"id": 1, "splits": 0}, {"id": 2, "splits": 0}, {"id": 7, "splits": 4}], "run 3", None, "USD", "eu", ), ( 1, 3, "1,2,3", "Q1 1999", [{"id": 42, "splits": 4}], "run 4", None, "USD", "us", ), ( 1, 3, "1,2,3", "Q1 1999", [{"id": 42, "splits": 4}], "run 4", [1, 8, 42], "USD", "eu", ), ( 123, 127, "123,124,125,126,127", "Mar 2009 - Feb 2010", [{"id": 42, "splits": 4}], "run 5", [1, 8, 42], "AUD", "eu", ), ( 123, 127, "123,124,125,126,127", "Mar 2009 - Feb 2010", [{"id": 42, "splits": 4}], "run 5", [1, 8, 42], "AUD", "eu", ), ( 123, 127, "123,124,125,126,127", "Mar 2009 - Feb 2010", [{"id": 42, "splits": 4}], "run 5", [1, 8, 42], "AUD", "eu", ), ( 123, 127, "123,124,125,126,127", "Mar 2009 - Feb 2010", [{"id": 42, "splits": 4}], "run 5", [1, 8, 42], "AUD", "eu", ), ], ) def test_trigger_report_generation( mocker, mock_account, mock_user, first_period_id, last_period_id, period_ids, period_name, collaborators, report_run_name, collaborator_ids, currency, number_format, ): """Test triggering report generation.""" client_email = "test@test.com" mock_get_for_account = mocker.patch.object( reports.CollaboratorPersister, "get_with_split_count" ) mock_get_for_account.return_value = collaborators mock_get_identity_metadata = mocker.patch.object(ows_users, "get_identity_metadata") mock_get_identity_metadata.return_value = { "email": client_email, "number_format": number_format, } mock_get_currencies_for_period_range = mocker.patch.object( ows_moneyhub, "get_currencies_for_period_range" ) mock_get_currencies_for_period_range.return_value = [currency] filtered_collaborators = [item for item in collaborators if item["splits"] > 0] correlation_id = uuid.uuid4() g_mock = mocker.patch.object(flask, "g") g_mock.correlation_id = correlation_id mock_create_report_run = mocker.patch.object( report_run_persister.ReportRunPersister, "create_report_run", return_value=( report_fixtures.created_report_run, report_fixtures.created_reports, ), ) mocker.patch.object(reports, "send_bulk_report_trigger_message") result = reports.trigger_report_generation( mock_account, mock_user, first_period_id, last_period_id, period_name, report_run_name, collaborator_ids, ) assert result == { "items": [report.to_dict() for report in report_fixtures.created_reports] } mock_get_for_account.assert_called_with( account=mock_account, collaborator_ids=collaborator_ids ) mock_create_report_run.assert_called_with( collaborators=filtered_collaborators, currency=currency, period_ids=period_ids, period_name=period_name, name=report_run_name, migrated_to_abacus=True, requestor_identity_uuid=mock_user.id, notification_email=client_email, number_format=number_format, ) assert reports.send_bulk_report_trigger_message.call_count == 1 reports.send_bulk_report_trigger_message.assert_called_with( correlation_id, report_run_uuid=report_fixtures.created_report_run.uuid, ) @pytest.mark.parametrize( [ "first_period_id", "last_period_id", "period_name", "collaborator_ids", "report_run_name", "accounting_intervals", ], [ ( 1, 1, "Jan 1999", [], "run 1", fixtures.ACCOUNTING_INTERVALS_OWS_CONTRACTS, ), ( 1, 3, "Q1 1999", [1, 2], "run 2", fixtures.ACCOUNTING_INTERVALS_QUARTERLY_OWS_CONTRACTS, ), ], ) def test_trigger_report_generation_no_splits( mocker, mock_account, mock_user, first_period_id, last_period_id, period_name, collaborator_ids, report_run_name, accounting_intervals, ): """Test triggering report generation.""" client_email = "test@test.com" mock_get_identity_metadata = mocker.patch.object(ows_users, "get_identity_metadata") mock_get_identity_metadata.return_value = { "email": client_email, "number_format": "us", } mock_get_currencies_for_period_range = mocker.patch.object( ows_moneyhub, "get_currencies_for_period_range" ) mock_get_currencies_for_period_range.return_value = ["USD"] mock_get_for_account = mocker.patch.object( reports.CollaboratorPersister, "get_with_split_count" ) mock_get_for_account.return_value = [] mock_send_bulk_report_trigger_message = mocker.patch.object( reports, "send_bulk_report_trigger_message" ) result = reports.trigger_report_generation( account=mock_account, user=mock_user, first_period_id=first_period_id, last_period_id=last_period_id, period_name=period_name, report_run_name=report_run_name, collaborator_ids=collaborator_ids, ) assert result is None mock_send_bulk_report_trigger_message.assert_not_called() @pytest.mark.parametrize( ( "first_period_id", "last_period_id", "period_name", "collaborator_ids", "report_run_name", "currencies", ), [ (1, 5, "Jan 1999", [], "run 1", ["GBP", "NZD"]), (1, 6, "Q1 1999", [1, 2], "run 2", ["GBP", "NZD"]), ], ) def test_trigger_report_generation_different_currency( mocker, mock_account, mock_user, first_period_id, last_period_id, period_name, collaborator_ids, report_run_name, currencies, ): """Test triggering report generation.""" client_email = "test@test.com" mock_get_identity_metadata = mocker.patch.object(ows_users, "get_identity_metadata") mock_get_identity_metadata.return_value = { "email": client_email, "number_format": "us", } mock_get_currencies_for_period_range = mocker.patch.object( ows_moneyhub, "get_currencies_for_period_range" ) mock_get_currencies_for_period_range.return_value = currencies with pytest.raises(OwsError) as exc_info: reports.trigger_report_generation( account=mock_account, user=mock_user, first_period_id=first_period_id, last_period_id=last_period_id, period_name=period_name, report_run_name=report_run_name, collaborator_ids=collaborator_ids, ) assert exc_info.value.status == 400 mock_get_currencies_for_period_range.assert_called_once_with( mock_account.id, first_period_id, last_period_id ) def test_trigger_report_generation_period_range_too_long(mock_account, mock_user): """Test when the period range supplied is too long.""" with pytest.raises(OwsError) as exc_info: reports.trigger_report_generation( mock_account, mock_user, 123, 135, "Mar 2009 - Mar 2010", "this run is tooooooo long", [], ) assert exc_info.value.status == 400 assert exc_info.value.code == error.ERROR_CODE_REPORT_PERIOD_RANGE_TOO_LONG @pytest.mark.parametrize( ( "first_period_id", "last_period_id", "period_ids", "period_name", "collaborators", "report_run_name", "collaborator_ids", "currency", ), [ ( 1, 3, "1,2,3", "Q1 1999", [{"id": 1, "splits": 2}, {"id": 2, "splits": 1}], "run 2", None, "USD", ), ], ) def test_trigger_report_generation_sqs_error( mocker, mock_account, mock_user, first_period_id, last_period_id, period_ids, period_name, collaborators, report_run_name, collaborator_ids, currency, ): """Test when an SQS error occurs while queuing the message.""" client_email = "test@test.com" number_format = "us" mock_get_for_account = mocker.patch.object( reports.CollaboratorPersister, "get_with_split_count" ) mock_get_for_account.return_value = collaborators mock_get_identity_metadata = mocker.patch.object(ows_users, "get_identity_metadata") mock_get_identity_metadata.return_value = { "email": client_email, "number_format": number_format, } filtered_collaborators = [item for item in collaborators if item["splits"] > 0] mock_get_currencies_for_period_range = mocker.patch.object( ows_moneyhub, "get_currencies_for_period_range" ) mock_get_currencies_for_period_range.return_value = [currency] correlation_id = uuid.uuid4() g_mock = mocker.patch.object(flask, "g") g_mock.correlation_id = correlation_id mock_create_report_run = mocker.patch.object( report_run_persister.ReportRunPersister, "create_report_run", return_value=( report_fixtures.created_report_run, report_fixtures.created_reports, ), ) mock_update_report_status = mocker.patch.object( report_persister.ReportPersister, "update_report_status" ) mock_send_bulk_report_trigger_message = mocker.patch.object( reports, "send_bulk_report_trigger_message", side_effect=Exception ) result = reports.trigger_report_generation( account=mock_account, user=mock_user, first_period_id=first_period_id, last_period_id=last_period_id, period_name=period_name, report_run_name=report_run_name, collaborator_ids=collaborator_ids, ) assert result == { "items": [report.to_dict() for report in report_fixtures.created_reports] } mock_get_for_account.assert_called_with( account=mock_account, collaborator_ids=collaborator_ids ) mock_create_report_run.assert_called_with( collaborators=filtered_collaborators, currency=currency, period_ids=period_ids, period_name=period_name, name=report_run_name, migrated_to_abacus=True, requestor_identity_uuid=mock_user.id, notification_email=client_email, number_format=number_format, ) assert mock_update_report_status.call_count == len(report_fixtures.created_reports) for report in report_fixtures.created_reports: mock_update_report_status.assert_any_call(report.report_id, "ERROR") assert mock_send_bulk_report_trigger_message.call_count == 1 mock_send_bulk_report_trigger_message.assert_called_with( correlation_id, report_run_uuid=report_fixtures.created_report_run.uuid, ) mock_get_currencies_for_period_range.assert_called_once_with( mock_account.id, first_period_id, last_period_id ) def test_report_download_file_not_found(mocker: MockFixture, mock_account: Account): """Test when report file is not found.""" mocker.patch.object( report_persister.ReportPersister, "get_item", return_value={ "file_location": "https://yr-bucket.s3.amazonaws.com/super/file.xls" }, ) mock_object_exists = mocker.patch.object(s3, "object_exists") mock_object_exists.return_value = False with pytest.raises(OwsError) as exc_info: reports.get_report_download(mock_account, 2) mock_object_exists.assert_called_with("yr-bucket", "super/file.xls") assert exc_info.value.code == error.ERROR_CODE_REPORT_FILE_NOT_FOUND def test_report_download_success(mocker: MockFixture, mock_account: Account): """Test successful report download URL generation.""" mocker.patch.object( report_persister.ReportPersister, "get_item", return_value={ "file_location": "https://yr-bucket.s3.amazonaws.com/super/file.xls" }, ) mock_object_exists = mocker.patch.object(s3, "object_exists") mock_object_exists.return_value = True mock_get_presigned_url = mocker.patch.object(s3, "get_presigned_url") mock_get_presigned_url.return_value = "http://presigned-url.com/woohoo" download_response = reports.get_report_download(mock_account, 2) mock_object_exists.assert_called_with("yr-bucket", "super/file.xls") mock_get_presigned_url.assert_called_with("yr-bucket", "super/file.xls", 500) assert download_response == {"url": "http://presigned-url.com/woohoo"} class MockReportRun: """Mock class for ReportRun.""" def __init__(self, trigger_type): """Initialize the mock ReportRun.""" self.trigger_type = trigger_type @pytest.mark.parametrize("report_data", [{"report_ids": [123, 456]}]) def test_bulk_delete_reports_success(report_data, mocker, mock_account, mock_user): """Test successfully deleting a report.""" mock_bulk_delete_items = mocker.patch.object( report_persister.ReportPersister, "bulk_delete_items" ) mocker.patch.object( report_persister.ReportPersister, "get_reports_with_report_ids", return_value=[{"report_run_id": 1}, {"report_run_id": 2}], ) mocker.patch.object( report_run_persister.ReportRunPersister, "get_report_runs_by_ids", return_value=[ MockReportRun("MANUAL"), MockReportRun("MANUAL"), ], ) bulk_delete_response = reports.bulk_delete_reports( mock_account, report_data, mock_user ) mock_bulk_delete_items.assert_called_with(mock_account, report_data, mock_user) assert bulk_delete_response is None @pytest.mark.parametrize("report_data", [{"report_ids": [123, 456]}]) def test_bulk_delete_reports_fail(report_data, mocker, mock_account, mock_user): """Test when trying to delete a report with an invalid report run type.""" mocker.patch.object(report_persister.ReportPersister, "bulk_delete_items") mocker.patch.object( report_persister.ReportPersister, "get_reports_with_report_ids", return_value=[{"report_run_id": 1}, {"report_run_id": 2}], ) mocker.patch.object( report_run_persister.ReportRunPersister, "get_report_runs_by_ids", return_value=[ MockReportRun("MANUAL"), MockReportRun("AUTO"), ], ) with pytest.raises(OwsError) as exc_info: reports.bulk_delete_reports(mock_account, report_data, mock_user) assert exc_info.value.status == 400 assert exc_info.value.code == error.ERROR_CANT_DELETE_AUTOMATIC_REPORT def test_get_latest_reports_by_collaborator_ids(mocker): """Test getting latest reports by collaborator ids.""" authorized_resources = [Resource(type="Vendor", id="100")] MockRow = namedtuple("MockRow", ["report_id", "collaborator_id", "vendor_id"]) get_latest_reports_by_collaborator_ids_mock = mocker.patch.object( reports.ReportPersister, "get_latest_reports_by_collaborator_ids", return_value=[ MockRow(report_id=1, collaborator_id=10, vendor_id=100), MockRow(report_id=2, collaborator_id=11, vendor_id=100), MockRow(report_id=3, collaborator_id=12, vendor_id=101), ], ) check_vendors_authorization_mock = mocker.patch.object( reports, "check_vendors_authorization", return_value=[100] ) results = reports.get_latest_reports_by_collaborator_ids( authorized_resources=authorized_resources, collaborator_ids=[10, 11, 12] ) assert results == [ {"data": {"id": 1}}, {"data": {"id": 2}}, {"data": None}, ] get_latest_reports_by_collaborator_ids_mock.assert_called_with([10, 11, 12]) check_vendors_authorization_mock.assert_called_with( authorized_resources, [100, 101], throw_if_unauthorized=False ) def test_get_report_contract_subtotals(mocker): """Test getting report contract subtotals.""" authorized_resources = [ Resource(type="Vendor", id="24601"), Resource(type="Vendor", id="120594"), ] MockRow = namedtuple( "MockRow", ["report_id", "contract_id", "currency", "amount", "vendor_id"] ) get_report_contract_subtotals_mock = mocker.patch.object( reports.ReportPersister, "get_report_contract_subtotals", return_value=[ MockRow( report_id=1, contract_id=1001, currency="USD", amount=500.0, vendor_id=24601, ), MockRow( report_id=1, contract_id=1002, currency="USD", amount=300.0, vendor_id=24601, ), MockRow( report_id=2, contract_id=1003, currency="EUR", amount=800.0, vendor_id=120594, ), MockRow( report_id=3, contract_id=1004, currency="GBP", amount=100.0, vendor_id=999, # Unauthorized vendor ), ], ) check_vendors_authorization_mock = mocker.patch.object( reports, "check_vendors_authorization", return_value=[24601, 120594] ) requested_report_ids = [1, 2, 3] results = reports.get_report_contract_subtotals_dataloader( requested_report_ids, authorized_resources, ) get_report_contract_subtotals_mock.assert_called_with( report_ids=requested_report_ids ) check_vendors_authorization_mock.assert_called_with( authorized_resources, [24601, 24601, 120594, 999], throw_if_unauthorized=False ) assert results == [ { "data": [ {"contract_id": 1001, "subtotal": {"currency": "USD", "amount": 500.0}}, {"contract_id": 1002, "subtotal": {"currency": "USD", "amount": 300.0}}, ] }, { "data": [ {"contract_id": 1003, "subtotal": {"currency": "EUR", "amount": 800.0}} ] }, {"data": []}, ] MockAggregation = namedtuple( "MockAggregation", [ "total_count", "currency_agnostic_total_amount", ], ) def test_get_report_contract_subtotal_aggregations(mocker): """Test getting report contract subtotal aggregations.""" mock_aggregation = MockAggregation( total_count=5, currency_agnostic_total_amount=1200.50 ) mock_get_aggregations = mocker.patch.object( report_persister.ReportPersister, "get_report_contract_subtotal_aggregations", return_value=mock_aggregation, ) result = reports.get_report_contract_subtotal_aggregations( report_run_id=2, collaborator_dp_enabled=True ) mock_get_aggregations.assert_called_with(2, True) assert result.total_count == 5 assert result.currency_agnostic_total_amount == 1200.50