From 7cf6a75da2d4a4858afbe754303a5f25c897e226 Mon Sep 17 00:00:00 2001 From: ruuushhh <66899387+ruuushhh@users.noreply.github.com> Date: Tue, 13 Feb 2024 16:13:09 +0530 Subject: [PATCH 1/5] Prod deploy GA (#95) * Fix Creds for connection call * Prod Deploye GA added * Remove In deployment --- .github/workflows/production_deployment.yml | 68 +++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 .github/workflows/production_deployment.yml diff --git a/.github/workflows/production_deployment.yml b/.github/workflows/production_deployment.yml new file mode 100644 index 0000000..fe1b343 --- /dev/null +++ b/.github/workflows/production_deployment.yml @@ -0,0 +1,68 @@ +name: Deploy to Production + +on: + release: + types: [created] + +jobs: + production_deploy: + runs-on: ubuntu-latest + environment: Production + steps: + - uses: actions/checkout@v2 + - name: push to dockerhub + uses: fylein/docker-release-action@master + env: + DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} + DOCKERHUB_PASSWORD: ${{ secrets.DOCKERHUB_PASSWORD }} + IMAGE_NAME: fyle_ms-business-central-api + + - name: Install kustomize + run: | + curl -s "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh" | bash + sudo mv kustomize /usr/local/bin/ + + - name: Clone another repository + uses: actions/checkout@v2 + with: + repository: ${{ secrets.PROD_DEPLOY_REPO }} + ref: master + path: ${{ secrets.PROD_DEPLOY_REPO }} + persist-credentials: false + token: ${{ secrets.DEPLOY_GIT_ACCESS_TOKEN }} + + - name: Update Image Tag + run: | + NEW_TAG="v$(git rev-parse --short HEAD)" + cd ${{ secrets.PROD_DEPLOY_REPO }}/${{ secrets.US_EKS_CLUSTER_NAME }}/integrations + kustomize edit set image docker.io/${{ secrets.DOCKERHUB_USERNAME }}/fyle_ms-business-central-api=docker.io/${{ secrets.DOCKERHUB_USERNAME }}/fyle_ms-business-central-api:$NEW_TAG + + - name: Commit and push changes + run: | + cd ${{ secrets.PROD_DEPLOY_REPO }}/ + git config --global user.email "integrations@fylehq.com" + git config --global user.name "GitHub Actions" + git add . + git commit -m "Update ms-business-central-api image tag" + git remote set-url origin https://x-access-token:${{ secrets.DEPLOY_GIT_ACCESS_TOKEN }}@github.com/${{ secrets.PROD_DEPLOY_REPO }} + git push origin master + git push origin master + + - name: Create new Sentry release + env: + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + SENTRY_ORG: fyle-technologies-private-limi + SENTRY_PROJECT: ms-business-central-api + SENTRY_DEPLOY_ENVIRONMENT: production + run: | + # Install Sentry CLI + curl -sL https://sentry.io/get-cli/ | bash + + # Create new Sentry release + export SENTRY_RELEASE=$(sentry-cli releases propose-version) + sentry-cli releases new -p $SENTRY_PROJECT $SENTRY_RELEASE + sentry-cli releases set-commits --auto $SENTRY_RELEASE + sentry-cli releases finalize $SENTRY_RELEASE + + # Create new deploy for this Sentry release + sentry-cli releases deploys $SENTRY_RELEASE new -e $SENTRY_DEPLOY_ENVIRONMENT From bd9716afad427c1102f65cfc4c07f14ff3fef409 Mon Sep 17 00:00:00 2001 From: Hrishabh Tiwari <74908943+Hrishabh17@users.noreply.github.com> Date: Wed, 14 Feb 2024 20:49:57 +0530 Subject: [PATCH 2/5] Added RetryException handler and bumped the sdk versions (#96) --- apps/fyle/exceptions.py | 8 +++++++- apps/mappings/exceptions.py | 6 +++++- requirements.txt | 4 ++-- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/apps/fyle/exceptions.py b/apps/fyle/exceptions.py index 469b0b8..fa12203 100644 --- a/apps/fyle/exceptions.py +++ b/apps/fyle/exceptions.py @@ -2,7 +2,7 @@ import traceback from functools import wraps -from fyle.platform.exceptions import NoPrivilegeError +from fyle.platform.exceptions import NoPrivilegeError, RetryException from apps.workspaces.models import FyleCredential @@ -27,6 +27,12 @@ def wrapper(*args, **kwargs): args[1].status = 'FAILED' args[1].save() + except RetryException: + logger.info('Fyle Retry Exception occured') + args[1].detail = {'message': 'Fyle Retry Exception occured'} + args[1].status = 'FATAL' + args[1].save() + except Exception: error = traceback.format_exc() args[1].detail = {'error': error} diff --git a/apps/mappings/exceptions.py b/apps/mappings/exceptions.py index f04a74d..226cf8d 100644 --- a/apps/mappings/exceptions.py +++ b/apps/mappings/exceptions.py @@ -4,7 +4,7 @@ from dynamics.exceptions.dynamics_exceptions import InvalidTokenError from fyle.platform.exceptions import InternalServerError from fyle.platform.exceptions import InvalidTokenError as FyleInvalidTokenError -from fyle.platform.exceptions import WrongParamsError +from fyle.platform.exceptions import WrongParamsError, RetryException from apps.mappings.models import ImportLog from apps.workspaces.models import BusinessCentralCredentials @@ -42,6 +42,10 @@ def new_fn(expense_attribute_instance, *args): error['alert'] = False import_log.status = 'FAILED' + except RetryException: + error['message'] = 'Fyle Retry Exception occured' + import_log.status = 'FATAL' + except InternalServerError: error['message'] = 'Internal server error while importing to Fyle' error['alert'] = True diff --git a/requirements.txt b/requirements.txt index 7326e99..0269270 100644 --- a/requirements.txt +++ b/requirements.txt @@ -25,7 +25,7 @@ gevent==23.9.1 gunicorn==20.1.0 # Platform SDK -fyle==0.36.0 +fyle==0.36.1 # Business central sdk ms-dynamics-business-central-sdk==1.4.2 @@ -33,7 +33,7 @@ ms-dynamics-business-central-sdk==1.4.2 # Reusable Fyle Packages fyle-rest-auth==1.6.0 fyle-accounting-mappings==1.29.2 -fyle-integrations-platform-connector==1.36.1 +fyle-integrations-platform-connector==1.36.3 # Postgres Dependincies psycopg2-binary==2.9.9 From b312a3728724d0d32a4a33edabd47dc71ecb10b6 Mon Sep 17 00:00:00 2001 From: Hrishabh Tiwari <74908943+Hrishabh17@users.noreply.github.com> Date: Mon, 19 Feb 2024 21:46:54 +0530 Subject: [PATCH 3/5] T1: Added Unit Test for journal_entry and purchase_invoice models (#97) --- .coveragerc | 2 +- apps/business_central/exports/base_model.py | 2 +- .../exports/purchase_invoice/models.py | 2 +- pytest.ini | 2 +- tests/test_business_central/conftest.py | 143 +++++++++++++ tests/test_business_central/fixtures.py | 147 +++++++++++++ tests/test_business_central/test_models.py | 197 ++++++++++++++++++ 7 files changed, 491 insertions(+), 4 deletions(-) create mode 100644 tests/test_business_central/conftest.py create mode 100644 tests/test_business_central/fixtures.py create mode 100644 tests/test_business_central/test_models.py diff --git a/.coveragerc b/.coveragerc index adf4f43..26944c1 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,2 +1,2 @@ [run] -omit = ./venv/*, ms_business_central_api/*, *forms.py, *apps.py,*manage.py,*__init__.py,apps/*/migrations/*.py,*asgi*,*wsgi*,*admin.py,*urls.py,*settings.py, *gunicorn*, \ No newline at end of file +omit = ./venv/*,*forms.py, *apps.py,*manage.py,*__init__.py,apps/*/migrations/*.py,*asgi*,*wsgi*,*admin.py,*urls.py,*settings.py, *gunicorn*, \ No newline at end of file diff --git a/apps/business_central/exports/base_model.py b/apps/business_central/exports/base_model.py index 0f17d58..6fa082a 100644 --- a/apps/business_central/exports/base_model.py +++ b/apps/business_central/exports/base_model.py @@ -111,7 +111,7 @@ def get_vendor_or_employee_id(employee_field_mapping: str, mapping: EmployeeMapp return "Employee", mapping.destination_employee.destination_id if export_settings.reimbursable_expenses_export_type == 'PURCHASE_INVOICE' and accounting_export.fund_source == 'PERSONAL': - return mapping.destination_vendor.destination_id + return "", mapping.destination_vendor.destination_id else: if accounting_export.fund_source == 'PERSONAL': return get_vendor_or_employee_id(export_settings.employee_field_mapping, mapping) diff --git a/apps/business_central/exports/purchase_invoice/models.py b/apps/business_central/exports/purchase_invoice/models.py index 1c0bdad..b52d027 100644 --- a/apps/business_central/exports/purchase_invoice/models.py +++ b/apps/business_central/exports/purchase_invoice/models.py @@ -33,7 +33,7 @@ def create_or_update_object(self, accounting_export: AccountingExport, _: Advanc :return: purchase invoices object """ - vendor_id = self.get_account_id_type(accounting_export=accounting_export, export_settings=export_settings) + _, vendor_id = self.get_account_id_type(accounting_export=accounting_export, export_settings=export_settings) amount = self.get_total_amount(accounting_export=accounting_export) invoice_date = self.get_invoice_date(accounting_export=accounting_export) diff --git a/pytest.ini b/pytest.ini index 23f16a0..a5f1cba 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,7 +1,7 @@ [pytest] DJANGO_SETTINGS_MODULE = ms_business_central_api.tests.settings python_files = tests.py test_*.py *_tests.py -addopts = -p no:warnings --strict-markers --no-migrations --create-db +addopts = -p no:warnings --strict-markers --no-migrations --create-db --cov=. --cov-config=.coveragerc log_cli = 1 log_cli_level = INFO log_cli_format = %(asctime)s [%(levelname)8s] %(name)s: %(message)s (%(filename)s:%(lineno)s) diff --git a/tests/test_business_central/conftest.py b/tests/test_business_central/conftest.py new file mode 100644 index 0000000..cd740f4 --- /dev/null +++ b/tests/test_business_central/conftest.py @@ -0,0 +1,143 @@ +import pytest +from apps.accounting_exports.models import AccountingExport +from apps.workspaces.models import AdvancedSetting, ExportSetting +from apps.fyle.models import Expense +from fyle_accounting_mappings.models import ( + ExpenseAttribute, + DestinationAttribute, + EmployeeMapping, + CategoryMapping +) +from apps.workspaces.models import Workspace + +from .fixtures import data + + +@pytest.fixture() +@pytest.mark.django_db(databases=['default']) +def create_export_settings(): + export_settings_data = data['export_settings'] + export_settings = ExportSetting.objects.create(**export_settings_data) + + return export_settings + + +@pytest.fixture() +@pytest.mark.django_db(databases=['default']) +def add_advanced_settings(): + advanced_settings_data = data['advanced_setting'] + advanced_settings = AdvancedSetting.objects.create(**advanced_settings_data) + + return advanced_settings + + +@pytest.fixture() +@pytest.mark.django_db(databases=['default']) +def create_expense_objects(): + workspace_id = 1 + expenses = data['expenses'] + expense_objects = Expense.create_expense_objects(expenses=expenses, workspace_id=workspace_id) + + return expense_objects + + +@pytest.fixture() +@pytest.mark.django_db(databases=['default']) +def create_accounting_export_expenses(create_expense_objects): + workspace_id = 1 + fund_source = 'PERSONAL' + expense_objects = create_expense_objects + AccountingExport.create_accounting_export(expense_objects=expense_objects, fund_source=fund_source, workspace_id=workspace_id) + accounting_export = AccountingExport.objects.all().first() + + return accounting_export + + +@pytest.fixture() +@pytest.mark.django_db(databases=['default']) +def create_expense_attribute(): + expense_attribute_data = data['employee_expense_attributes'] + expense_attribute_data['workspace'] = Workspace.objects.get(id=1) + expense_attribute = ExpenseAttribute.objects.create(**expense_attribute_data) + + return expense_attribute + + +@pytest.fixture() +@pytest.mark.django_db(databases=['default']) +def create_destination_attribute(): + destination_emp_data = data['employee_destination_attributes'] + destination_emp_data['workspace'] = Workspace.objects.get(id=1) + DestinationAttribute.objects.create(**destination_emp_data) + + destination_vendor_data = data['vendor_destination_attributes'] + destination_vendor_data['workspace'] = Workspace.objects.get(id=1) + DestinationAttribute.objects.create(**destination_vendor_data) + + +@pytest.fixture() +@pytest.mark.django_db(databases=['default']) +def create_employee_mapping_with_employee(create_expense_attribute, create_destination_attribute): + source_field_id = ExpenseAttribute.objects.get(source_id='source123') + destination_field_id = DestinationAttribute.objects.get(destination_id='destination123') + workspace = Workspace.objects.get(id=1) + + employee_employee_mapping = { + 'source_employee': source_field_id, + 'destination_employee': destination_field_id, + 'workspace': workspace, + } + + EmployeeMapping.objects.create(**employee_employee_mapping) + + +@pytest.fixture() +@pytest.mark.django_db(databases=['default']) +def create_employee_mapping_with_vendor(create_expense_attribute, create_destination_attribute): + source_field_id = ExpenseAttribute.objects.get(source_id='source123') + destination_field_id = DestinationAttribute.objects.get(destination_id='dest_vendor123') + workspace = Workspace.objects.get(id=1) + + employee_vendor_mapping = { + 'source_employee': source_field_id, + 'destination_vendor': destination_field_id, + 'workspace': workspace, + } + + EmployeeMapping.objects.create(**employee_vendor_mapping) + + +@pytest.fixture() +@pytest.mark.django_db(databases=['default']) +def create_source_category_attribute(): + source_category_attribute_data = data['category_expense_attributes'] + source_category_attribute_data['workspace'] = Workspace.objects.get(id=1) + source_category_attribute = ExpenseAttribute.objects.create(**source_category_attribute_data) + + return source_category_attribute + + +@pytest.fixture() +@pytest.mark.django_db(databases=['default']) +def create_destination_category_attribute(): + destination_category_attribute_data = data['category_destination_attributes'] + destination_category_attribute_data['workspace'] = Workspace.objects.get(id=1) + destination_category_attribute = DestinationAttribute.objects.create(**destination_category_attribute_data) + + return destination_category_attribute + + +@pytest.fixture() +@pytest.mark.django_db(databases=['default']) +def create_category_mapping(create_source_category_attribute, create_destination_category_attribute): + source_category = ExpenseAttribute.objects.get(source_id='src_category123') + destination_category = DestinationAttribute.objects.get(destination_id='dest_category123') + workspace = Workspace.objects.get(id=1) + + category_mapping = CategoryMapping.create_or_update_category_mapping( + source_category_id=source_category.id, + destination_account_id=destination_category.id, + workspace=workspace + ) + + return category_mapping diff --git a/tests/test_business_central/fixtures.py b/tests/test_business_central/fixtures.py new file mode 100644 index 0000000..7205911 --- /dev/null +++ b/tests/test_business_central/fixtures.py @@ -0,0 +1,147 @@ +data = { + "accounting_export": { + "type": "JOURNAL_ENTRY", + "fund_source": "PERSONAL", + "mapping_errors": ["Error 1", "Error 2"], + "expenses": ["Expense1", "Expense2"], + "task_id": "task123", + "export_url": "https://example.com/export", + "description": { + "expense_id": "ABCDEF123", + "fund_source": "PERSONAL", + "employee_email": "ashwin.t@fyle.in", + "expense_number": "E/2023/04/T/31" + }, + "status": "Exported", + "detail": {"key": "value"}, + "business_central_errors": {"error": "Something went wrong"}, + "exported_at": "2024-02-15T12:00:00Z" + }, + "export_settings": { + "workspace_id": 1, + "reimbursable_expenses_export_type": "JOURNAL_ENTRY", + "default_bank_account_name": "Bank of Example", + "default_bank_account_id": "123456789", + "reimbursable_expense_state": "PAYMENT_PROCESSING", + "reimbursable_expense_date": "LAST_SPENT_AT", + "reimbursable_expense_grouped_by": "REPORT", + "credit_card_expense_export_type": "JOURNAL_ENTRY", + "credit_card_expense_state": "APPROVED", + "credit_card_expense_grouped_by": "REPORT", + "credit_card_expense_date": "LAST_SPENT_AT", + "default_vendor_name": "Vendor X", + "default_vendor_id": "987654321", + "journal_entry_folder_id": "folder123", + "employee_field_mapping": "EMPLOYEE", + "name_in_journal_entry": "EMPLOYEE", + "auto_map_employees": "NAME" + }, + "advanced_setting": { + "workspace_id": 1, + "expense_memo_structure": ["Field1", "Field2", "Field3"], + "schedule_is_enabled": True, + "start_datetime": "2024-02-15T08:00:00Z", + "schedule_id": "schedule123", + "interval_hours": 24, + "emails_selected": ["email1@example.com", "email2@example.com"], + "emails_added": ["newemail@example.com"], + "auto_create_vendor": False + }, + "employee_expense_attributes": { + "attribute_type": "Employee", + "value": "ashwin.t@fyle.in", + "display_name": "Employee One", + "active": True, + "source_id": "source123", + }, + "employee_destination_attributes": { + "attribute_type": "Employee", + "value": "hello.world@test.in", + "display_name": "Employee One", + "active": True, + "destination_id": "destination123", + }, + "vendor_destination_attributes": { + "attribute_type": "Vendor", + "value": "Ashwin", + "display_name": "Ashwin", + "active": True, + "destination_id": "dest_vendor123", + }, + "category_expense_attributes": { + "attribute_type": "Category", + "value": "Food", + "display_name": "Food", + "active": True, + "source_id": "src_category123", + }, + "category_destination_attributes": { + "attribute_type": "Category", + "value": "Food", + "display_name": "Food", + "active": True, + "destination_id": "dest_category123", + }, + "expenses":[ + { + 'id':91, + 'employee_email':'ashwin.t@fyle.in', + 'employee_name':'Joanna', + 'category':'Food', + 'sub_category':None, + 'project':'Aaron Abbott', + 'org_id':'or79Cob97KSh', + 'expense_id':'txxTi9ZfdepC', + 'expense_number':'E/2022/05/T/16', + 'claim_number':'C/2022/05/R/4', + 'report_title':'R/2022/05/R/4', + 'amount':50.0, + 'currency':'USD', + 'foreign_amount':None, + 'foreign_currency':None, + 'tax_amount':None, + 'tax_group_id':None, + 'settlement_id':'setDiksMn83K7', + 'reimbursable':True, + 'billable':False, + 'exported':False, + 'state':'PAYMENT_PROCESSING', + 'vendor':'Ashwin', + 'cost_center':'Marketing', + 'purpose':None, + 'report_id':'rpViBmuYmAgw', + 'corporate_card_id':None, + 'file_ids':[ + + ], + 'spent_at':'2022-05-13T17:00:00Z', + 'approved_at':'2022-05-13T09:30:13.484000Z', + 'posted_at': '2021-12-22T07:30:26.289842+00:00', + 'expense_created_at':'2022-05-13T09:29:43.535468Z', + 'expense_updated_at':'2022-05-13T09:32:06.643941Z', + 'created_at':'2022-05-23T11:11:28.241406Z', + 'updated_at':'2022-05-23T11:11:28.241440Z', + 'fund_source':'PERSONAL', + 'source_account_type': 'PERSONAL_CASH_ACCOUNT', + 'verified_at':'2022-05-23T11:11:28.241440Z', + 'custom_properties':{ + 'Team':'', + 'Class':'', + 'Klass':'', + 'Location':'', + 'Team Copy':'', + 'Tax Groups':'', + 'Departments':'', + 'Team 2 Postman':'', + 'User Dimension':'', + 'Location Entity':'', + 'Operating System':'', + 'System Operating':'', + 'User Dimension Copy':'', + 'Custom Expense Field':'None' + }, + 'paid_on_qbo':False, + 'payment_number':'P/2022/05/R/7' + } + ] +} diff --git a/tests/test_business_central/test_models.py b/tests/test_business_central/test_models.py new file mode 100644 index 0000000..e3acb54 --- /dev/null +++ b/tests/test_business_central/test_models.py @@ -0,0 +1,197 @@ +from apps.business_central.models import ( + JournalEntry, + JournalEntryLineItems, + PurchaseInvoice, + PurchaseInvoiceLineitems +) +from apps.workspaces.models import AdvancedSetting, ExportSetting +from apps.accounting_exports.models import AccountingExport +from fyle_accounting_mappings.models import EmployeeMapping + + +def test_create_or_update_journal_entry_1( + db, + create_temp_workspace, + create_export_settings, + add_advanced_settings, + create_accounting_export_expenses, + create_employee_mapping_with_employee +): + workspace_id = 1 + export_settings = ExportSetting.objects.get(workspace_id=workspace_id) + advanced_setting = AdvancedSetting.objects.get(workspace_id=workspace_id) + accounting_export = AccountingExport.objects.get(workspace_id=workspace_id) + + assert accounting_export.description.get('employee_email') == "ashwin.t@fyle.in" + + JournalEntry.create_or_update_object( + accounting_export=accounting_export, + _ = advanced_setting, + export_settings=export_settings + ) + + journal_entry = JournalEntry.objects.first() + assert journal_entry.accounting_export.workspace.id == 1 + assert journal_entry.amount == -50 + + +def test_create_or_update_journal_entry_2( + db, + create_temp_workspace, + create_export_settings, + add_advanced_settings, + create_accounting_export_expenses, + create_employee_mapping_with_vendor +): + workspace_id = 1 + export_settings = ExportSetting.objects.get(workspace_id=workspace_id) + advanced_setting = AdvancedSetting.objects.get(workspace_id=workspace_id) + accounting_export = AccountingExport.objects.get(workspace_id=workspace_id) + + assert accounting_export.description.get('employee_email') == "ashwin.t@fyle.in" + + export_settings.reimbursable_expenses_export_type = 'PURCHASE_INVOICE' + export_settings.save() + + mapping = EmployeeMapping.objects.filter( + source_employee__value=accounting_export.description.get('employee_email'), + workspace_id=accounting_export.workspace_id + ).first() + + assert mapping.destination_vendor.destination_id == "dest_vendor123" + + JournalEntry.create_or_update_object( + accounting_export=accounting_export, + _ = advanced_setting, + export_settings=export_settings + ) + + journal_entry = JournalEntry.objects.first() + assert journal_entry.accounting_export.workspace.id == 1 + assert journal_entry.amount == -50 + + +def test_create_or_update_journal_entry_3( + db, + create_temp_workspace, + create_export_settings, + add_advanced_settings, + create_accounting_export_expenses, + create_employee_mapping_with_vendor +): + workspace_id = 1 + export_settings = ExportSetting.objects.get(workspace_id=workspace_id) + advanced_setting = AdvancedSetting.objects.get(workspace_id=workspace_id) + accounting_export = AccountingExport.objects.get(workspace_id=workspace_id) + + assert accounting_export.description.get('employee_email') == "ashwin.t@fyle.in" + + export_settings.reimbursable_expenses_export_type = 'JOURNAL_ENTRY' + export_settings.name_in_journal_entry = 'MERCHANT' + export_settings.save() + + accounting_export.fund_source = 'CCC' + accounting_export.save() + + mapping = EmployeeMapping.objects.filter( + source_employee__value=accounting_export.description.get('employee_email'), + workspace_id=accounting_export.workspace_id + ).first() + + assert mapping.destination_vendor.destination_id == "dest_vendor123" + + JournalEntry.create_or_update_object( + accounting_export=accounting_export, + _ = advanced_setting, + export_settings=export_settings + ) + + journal_entry = JournalEntry.objects.first() + assert journal_entry.accounting_export.workspace.id == 1 + assert journal_entry.amount == -50 + + +def test_create_or_update_journal_entry_line_items( + db, + create_temp_workspace, + create_export_settings, + add_advanced_settings, + create_accounting_export_expenses, + create_employee_mapping_with_employee, + add_fyle_credentials, + create_category_mapping +): + workspace_id = 1 + + export_settings = ExportSetting.objects.get(workspace_id=workspace_id) + advanced_setting = AdvancedSetting.objects.get(workspace_id=workspace_id) + accounting_export = AccountingExport.objects.get(workspace_id=workspace_id) + + JournalEntry.create_or_update_object( + accounting_export=accounting_export, + _ = advanced_setting, + export_settings=export_settings + ) + + assert JournalEntry.objects.first().accounting_export.workspace.id == 1 + + journal_line_items = JournalEntryLineItems.create_or_update_object( + accounting_export=AccountingExport.objects.get(workspace_id=workspace_id), + advance_setting=AdvancedSetting.objects.get(workspace_id=workspace_id), + export_settings=ExportSetting.objects.get(workspace_id=workspace_id) + ) + + assert len(journal_line_items) == 1 + assert journal_line_items[0].journal_entry.accounting_export.workspace.id == 1 + assert journal_line_items[0].journal_entry.amount == -50 + + +def test_create_or_update_purchase_invoice( + db, + create_temp_workspace, + create_export_settings, + add_advanced_settings, + create_accounting_export_expenses, + create_employee_mapping_with_employee +): + workspace_id = 1 + + purchase_invoice = PurchaseInvoice.create_or_update_object( + accounting_export=AccountingExport.objects.get(workspace_id=workspace_id), + _=AdvancedSetting.objects.get(workspace_id=workspace_id), + export_settings=ExportSetting.objects.get(workspace_id=workspace_id) + ) + + assert purchase_invoice.accounting_export.workspace.id == 1 + assert purchase_invoice.amount == 50 + + +def test_create_or_update_purchase_invoice_line_items( + db, + create_temp_workspace, + create_export_settings, + add_advanced_settings, + create_accounting_export_expenses, + create_employee_mapping_with_employee, + add_fyle_credentials, + create_category_mapping +): + workspace_id = 1 + + PurchaseInvoice.create_or_update_object( + accounting_export=AccountingExport.objects.get(workspace_id=workspace_id), + _=AdvancedSetting.objects.get(workspace_id=workspace_id), + export_settings=ExportSetting.objects.get(workspace_id=workspace_id) + ) + + assert PurchaseInvoice.objects.first().accounting_export.workspace.id == 1 + + purchase_invoice_line_items = PurchaseInvoiceLineitems.create_or_update_object( + accounting_export=AccountingExport.objects.get(workspace_id=workspace_id), + advance_setting=AdvancedSetting.objects.get(workspace_id=workspace_id), + _=ExportSetting.objects.get(workspace_id=workspace_id) + ) + + assert len(purchase_invoice_line_items) == 1 + assert purchase_invoice_line_items[0].purchase_invoice.accounting_export.workspace.id == 1 + assert purchase_invoice_line_items[0].purchase_invoice.amount == 50 From f914b4973ea7bba9ab3fed755b5172c7a295767e Mon Sep 17 00:00:00 2001 From: Hrishabh Tiwari <74908943+Hrishabh17@users.noreply.github.com> Date: Thu, 22 Feb 2024 11:33:33 +0530 Subject: [PATCH 4/5] Add unit tests (#98) * T1: Added Unit Test for journal_entry and purchase_invoice models * T2: Added/Modified test cases for business_central exceptions, models and views * Revert "Merged master to add-unit-tests" This reverts commit 2ce3bb1613b0f02fe8ead5983762a862a2b59786, reversing changes made to 7633c4fd02dc41da1dbcf242e3ccdd65c547960a. --- tests/test_business_central/conftest.py | 116 +++++++++++++++- tests/test_business_central/fixtures.py | 8 +- .../test_business_central/test_exceptions.py | 106 ++++++++++++++ tests/test_business_central/test_models.py | 129 ++++++++++++++++++ tests/test_business_central/test_views.py | 69 ++++++++++ 5 files changed, 423 insertions(+), 5 deletions(-) create mode 100644 tests/test_business_central/test_exceptions.py diff --git a/tests/test_business_central/conftest.py b/tests/test_business_central/conftest.py index cd740f4..ee0622d 100644 --- a/tests/test_business_central/conftest.py +++ b/tests/test_business_central/conftest.py @@ -6,8 +6,11 @@ ExpenseAttribute, DestinationAttribute, EmployeeMapping, - CategoryMapping + CategoryMapping, + Mapping ) +from apps.business_central.models import JournalEntry, JournalEntryLineItems +from apps.business_central.models import PurchaseInvoice, PurchaseInvoiceLineitems from apps.workspaces.models import Workspace from .fixtures import data @@ -141,3 +144,114 @@ def create_category_mapping(create_source_category_attribute, create_destination ) return category_mapping + + +@pytest.fixture() +@pytest.mark.django_db(databases=['default']) +def create_journal_entry( + create_temp_workspace, + create_export_settings, + add_advanced_settings, + create_accounting_export_expenses, + create_employee_mapping_with_employee +): + workspace_id = 1 + export_settings = ExportSetting.objects.get(workspace_id=workspace_id) + advanced_setting = AdvancedSetting.objects.get(workspace_id=workspace_id) + accounting_export = AccountingExport.objects.get(workspace_id=workspace_id) + + JournalEntry.create_or_update_object( + accounting_export=accounting_export, + _=advanced_setting, + export_settings=export_settings + ) + + return JournalEntry.objects.first() + + +@pytest.fixture() +@pytest.mark.django_db(databases=['default']) +def create_journal_line_items( + add_fyle_credentials, + create_journal_entry, + create_category_mapping +): + workspace_id = 1 + export_settings = ExportSetting.objects.get(workspace_id=workspace_id) + advanced_setting = AdvancedSetting.objects.get(workspace_id=workspace_id) + accounting_export = AccountingExport.objects.get(workspace_id=workspace_id) + + journal_line_items = JournalEntryLineItems.create_or_update_object( + accounting_export=accounting_export, + advance_setting=advanced_setting, + export_settings=export_settings + ) + + return journal_line_items + + +@pytest.fixture() +@pytest.mark.django_db(databases=['default']) +def create_purchase_invoice( + create_temp_workspace, + create_export_settings, + add_advanced_settings, + create_employee_mapping_with_employee, + create_accounting_export_expenses, +): + workspace_id = 1 + export_settings = ExportSetting.objects.get(workspace_id=workspace_id) + advanced_setting = AdvancedSetting.objects.get(workspace_id=workspace_id) + accounting_export = AccountingExport.objects.get(workspace_id=workspace_id) + + PurchaseInvoice.create_or_update_object( + accounting_export=accounting_export, + _=advanced_setting, + export_settings=export_settings + ) + + return PurchaseInvoice.objects.first() + + +@pytest.fixture() +@pytest.mark.django_db(databases=['default']) +def create_purchase_invoice_line_items( + add_fyle_credentials, + create_purchase_invoice, + create_category_mapping +): + workspace_id = 1 + export_settings = ExportSetting.objects.get(workspace_id=workspace_id) + advanced_setting = AdvancedSetting.objects.get(workspace_id=workspace_id) + accounting_export = AccountingExport.objects.get(workspace_id=workspace_id) + + purchase_invoice_line_items = PurchaseInvoiceLineitems.create_or_update_object( + accounting_export=accounting_export, + advance_setting=advanced_setting, + _=export_settings + ) + + return purchase_invoice_line_items + + +@pytest.fixture() +@pytest.mark.django_db(databases=['default']) +def create_mapping_object( + create_temp_workspace, + create_expense_attribute, + create_destination_attribute +): + workspace_id = 1 + expense_attribute = ExpenseAttribute.objects.filter(workspace_id=workspace_id).first() + destination_attribute = DestinationAttribute.objects.filter(workspace_id=workspace_id).first() + workspace = Workspace.objects.get(id=workspace_id) + + mapping_object = Mapping.objects.create( + source_type=expense_attribute.attribute_type, + destination_type=destination_attribute.attribute_type, + source=expense_attribute, + destination=destination_attribute, + workspace=workspace + ) + + return mapping_object diff --git a/tests/test_business_central/fixtures.py b/tests/test_business_central/fixtures.py index 7205911..48701c8 100644 --- a/tests/test_business_central/fixtures.py +++ b/tests/test_business_central/fixtures.py @@ -48,14 +48,14 @@ "auto_create_vendor": False }, "employee_expense_attributes": { - "attribute_type": "Employee", + "attribute_type": "EMPLOYEE", "value": "ashwin.t@fyle.in", "display_name": "Employee One", "active": True, "source_id": "source123", }, "employee_destination_attributes": { - "attribute_type": "Employee", + "attribute_type": "EMPLOYEE", "value": "hello.world@test.in", "display_name": "Employee One", "active": True, @@ -69,14 +69,14 @@ "destination_id": "dest_vendor123", }, "category_expense_attributes": { - "attribute_type": "Category", + "attribute_type": "CATEGORY", "value": "Food", "display_name": "Food", "active": True, "source_id": "src_category123", }, "category_destination_attributes": { - "attribute_type": "Category", + "attribute_type": "CATEGORY", "value": "Food", "display_name": "Food", "active": True, diff --git a/tests/test_business_central/test_exceptions.py b/tests/test_business_central/test_exceptions.py new file mode 100644 index 0000000..bd9e155 --- /dev/null +++ b/tests/test_business_central/test_exceptions.py @@ -0,0 +1,106 @@ +from dynamics.exceptions.dynamics_exceptions import InvalidTokenError, WrongParamsError + +from apps.business_central.exceptions import ( + handle_business_central_exceptions, + handle_business_central_error +) +from apps.accounting_exports.models import AccountingExport, Error +from apps.workspaces.models import BusinessCentralCredentials, FyleCredential +from ms_business_central_api.exceptions import BulkError + + +def test_handle_business_central_error( + db, + create_temp_workspace, + create_export_settings, + create_accounting_export_expenses, +): + workspace_id = 1 + export_type = 'Journal Entry' + exception = WrongParamsError(response = 'Error', msg = 'Error') + + accounting_export = AccountingExport.objects.filter(workspace_id=workspace_id).first() + + handle_business_central_error(exception, accounting_export, export_type) + + error = Error.objects.filter(workspace_id=workspace_id, accounting_export=accounting_export).first() + accounting_export = AccountingExport.objects.filter(workspace_id=workspace_id).first() + + assert accounting_export.status == 'FAILED' + assert accounting_export.detail == None + + assert error.error_title == 'Failed to create Journal Entry' + assert error.type == 'BUSINESS_CENTRAL_ERROR' + assert error.error_detail == 'Error' + assert error.is_resolved == False + + +def test_handle_business_central_exceptions( + db, + mocker, + create_temp_workspace, + create_export_settings, + create_accounting_export_expenses, + add_accounting_export_summary, + add_business_central_creds + +): + workspace_id = 1 + accounting_export = AccountingExport.objects.filter(workspace_id=workspace_id).first() + + @handle_business_central_exceptions() + def test_func(accounting_export): + raise FyleCredential.DoesNotExist('Fyle credentials not found') + + test_func(accounting_export) + + assert accounting_export.status == 'FAILED' + assert accounting_export.detail == {'message': 'Fyle credentials do not exist in workspace'} + + @handle_business_central_exceptions() + def test_func(accounting_export): + raise BusinessCentralCredentials.DoesNotExist('Business Central Account not connected / token expired') + + test_func(accounting_export) + + assert accounting_export.status == 'FAILED' + assert accounting_export.detail == {'accounting_export_id': accounting_export.id, 'message': 'Business Central Account not connected / token expired'} + + @handle_business_central_exceptions() + def test_func(accounting_export): + raise WrongParamsError(response = 'Error', msg = 'Error') + + test_func(accounting_export) + + error = Error.objects.filter(workspace_id=workspace_id, accounting_export=accounting_export).first() + assert accounting_export.status == 'FAILED' + assert accounting_export.detail == None + assert error.type == 'BUSINESS_CENTRAL_ERROR' + assert error.is_resolved == False + + @handle_business_central_exceptions() + def test_func(accounting_export): + raise InvalidTokenError(response = 'Error', msg = 'Error') + + test_func(accounting_export) + + business_central_credentials = BusinessCentralCredentials.objects.filter(workspace_id=workspace_id).first() + assert business_central_credentials.is_expired == True + assert business_central_credentials.refresh_token == None + + @handle_business_central_exceptions() + def test_func(accounting_export): + raise BulkError(response = 'Error', msg = 'Error') + + test_func(accounting_export) + + assert accounting_export.status == 'FAILED' + assert accounting_export.detail == 'Error' + + @handle_business_central_exceptions() + def test_func(accounting_export): + raise Exception('Error') + + test_func(accounting_export) + + assert accounting_export.status == 'FATAL' diff --git a/tests/test_business_central/test_models.py b/tests/test_business_central/test_models.py index e3acb54..bb9100a 100644 --- a/tests/test_business_central/test_models.py +++ b/tests/test_business_central/test_models.py @@ -1,3 +1,5 @@ +import pytest + from apps.business_central.models import ( JournalEntry, JournalEntryLineItems, @@ -7,6 +9,7 @@ from apps.workspaces.models import AdvancedSetting, ExportSetting from apps.accounting_exports.models import AccountingExport from fyle_accounting_mappings.models import EmployeeMapping +from apps.business_central.exports.accounting_export import AccountingDataExporter def test_create_or_update_journal_entry_1( @@ -195,3 +198,129 @@ def test_create_or_update_purchase_invoice_line_items( assert len(purchase_invoice_line_items) == 1 assert purchase_invoice_line_items[0].purchase_invoice.accounting_export.workspace.id == 1 assert purchase_invoice_line_items[0].purchase_invoice.amount == 50 + + +def test_accounting_data_exporter_1(): + workspace_id = 1 + accounting_data_exporter = AccountingDataExporter() + + with pytest.raises(NotImplementedError): + accounting_data_exporter.post( + workspace_id=workspace_id, + body="Random body", + lineitems="Random lineitems" + ) + + assert True + + +def test_accounting_data_exporter_2( + db, + mocker, + create_temp_workspace, + create_export_settings, + add_advanced_settings, + create_accounting_export_expenses, + create_employee_mapping_with_employee, + create_category_mapping +): + workspace_id = 1 + + accounting_export = AccountingExport.objects.get(workspace_id=workspace_id) + + accounting_export.status = 'ENQUEUED' + accounting_export.save() + + accounting_data_exporter = AccountingDataExporter() + + mock_body_model = mocker.patch.object(accounting_data_exporter, 'body_model') + mock_lineitem_model = mocker.patch.object(accounting_data_exporter, 'lineitem_model') + + mocker.patch.object(mock_body_model, 'create_or_update_object') + mocker.patch.object(mock_lineitem_model, 'create_or_update_object') + + with pytest.raises(NotImplementedError): + accounting_data_exporter.create_business_central_object( + accounting_export=accounting_export + ) + + assert accounting_export.status == 'IN_PROGRESS' + assert mock_body_model.create_or_update_object.call_count == 1 + assert mock_lineitem_model.create_or_update_object.call_count == 1 + + +def test_accounting_data_exporter_3( + db, + mocker, + create_temp_workspace, + create_export_settings, + add_advanced_settings, + create_accounting_export_expenses, + create_employee_mapping_with_employee, + create_category_mapping +): + workspace_id = 1 + + accounting_export = AccountingExport.objects.get(workspace_id=workspace_id) + + accounting_export.status = 'ENQUEUED' + accounting_export.save() + + accounting_data_exporter = AccountingDataExporter() + + mock_body_model = mocker.patch.object(accounting_data_exporter, 'body_model') + mock_lineitem_model = mocker.patch.object(accounting_data_exporter, 'lineitem_model') + + mock_post = mocker.patch.object(accounting_data_exporter, 'post', return_value='Random response') + + mocker.patch.object(mock_body_model, 'create_or_update_object') + mocker.patch.object(mock_lineitem_model, 'create_or_update_object') + + accounting_data_exporter.create_business_central_object( + accounting_export=accounting_export + ) + + assert accounting_export.status == 'COMPLETE' + assert mock_body_model.create_or_update_object.call_count == 1 + assert mock_lineitem_model.create_or_update_object.call_count == 1 + assert mock_post.call_count == 1 + + assert accounting_export.detail == 'Random response' + assert accounting_export.export_url == 'https://businesscentral.dynamics.com/' + assert accounting_export.business_central_errors is None + + +def test_accounting_data_exporter_4( + db, + mocker, + create_temp_workspace, + create_export_settings, + add_advanced_settings, + create_accounting_export_expenses, + create_employee_mapping_with_employee, + create_category_mapping +): + workspace_id = 1 + + accounting_export = AccountingExport.objects.get(workspace_id=workspace_id) + + accounting_export.status = 'COMPLETE' + accounting_export.save() + + accounting_data_exporter = AccountingDataExporter() + + mock_body_model = mocker.patch.object(accounting_data_exporter, 'body_model') + mock_lineitem_model = mocker.patch.object(accounting_data_exporter, 'lineitem_model') + mock_post = mocker.patch.object(accounting_data_exporter, 'post', return_value='Random response') + + mocker.patch.object(mock_body_model, 'create_or_update_object') + mocker.patch.object(mock_lineitem_model, 'create_or_update_object') + + accounting_data_exporter.create_business_central_object( + accounting_export=accounting_export + ) + + assert accounting_export.status == 'COMPLETE' + assert mock_body_model.create_or_update_object.call_count == 0 + assert mock_lineitem_model.create_or_update_object.call_count == 0 + assert mock_post.call_count == 0 diff --git a/tests/test_business_central/test_views.py b/tests/test_business_central/test_views.py index a554052..87a0735 100644 --- a/tests/test_business_central/test_views.py +++ b/tests/test_business_central/test_views.py @@ -1,4 +1,5 @@ import json +import pytest from django.urls import reverse @@ -28,6 +29,25 @@ def test_sync_dimensions(api_client, test_connection, mocker, create_temp_worksp assert response['message'] == 'Business Central credentials not found / invalid in workspace' +def test_sync_dimensions_1(api_client, test_connection, mocker, create_temp_workspace, add_business_central_creds): + workspace_id = 1 + + access_token = test_connection.access_token + url = reverse('import-business-central-attributes', kwargs={'workspace_id': workspace_id}) + + api_client.credentials(HTTP_AUTHORIZATION='Bearer {}'.format(access_token)) + + mocker.patch('apps.business_central.helpers.sync_dimensions', return_value=None) + + mocker.patch('apps.workspaces.models.BusinessCentralCredentials.objects.get', side_effect=Exception("Unexpected error")) + + with pytest.raises(Exception): + response = api_client.post(url) + + assert response.status_code == 500 + assert response.data['message'] == 'Something unexpected happened' + + def test_business_central_fields(api_client, test_connection, create_temp_workspace, add_fyle_credentials, add_destination_attributes): workspace_id = 1 @@ -62,3 +82,52 @@ def test_post_company_selection(api_client, test_connection): response = api_client.post(url, payload) assert response.status_code == 201 + + +def test_connection_view_1(api_client, test_connection, create_temp_workspace): + workspace_id = 1 + url = reverse('connection', kwargs={'workspace_id': workspace_id}) + api_client.credentials(HTTP_AUTHORIZATION='Bearer {}'.format(test_connection.access_token)) + response = api_client.get(url) + + assert response.status_code == 400 + assert response.data['message'] == 'Business Central credentials not found in workspace' + + +def test_connection_view_2(api_client, test_connection, create_temp_workspace, add_business_central_creds, mocker): + workspace_id = 1 + url = reverse('connection', kwargs={'workspace_id': workspace_id}) + api_client.credentials(HTTP_AUTHORIZATION='Bearer {}'.format(test_connection.access_token)) + + business_central_credentials = BusinessCentralCredentials.objects.get(workspace_id=workspace_id) + + assert business_central_credentials.workspace.id == workspace_id + assert business_central_credentials.refresh_token == 'dummy_refresh_token' + assert business_central_credentials.is_expired == False + + response = api_client.get(url) + + assert response.status_code == 400 + assert response.data['message'] == 'Invalid token or Business Central connection expired' + + +def test_connection_view_3( + api_client, + test_connection, + create_temp_workspace, + add_business_central_creds, + mocker +): + workspace_id = 1 + url = reverse('connection', kwargs={'workspace_id': workspace_id}) + api_client.credentials(HTTP_AUTHORIZATION='Bearer {}'.format(test_connection.access_token)) + + mocker.patch( + 'apps.business_central.utils.BusinessCentralConnector.get_companies', + return_value=[{'id': '123', 'name': 'Fyle Technologies'}] + ) + + response = api_client.get(url) + + assert response.status_code == 200 + assert response.data == [{'id': '123', 'name': 'Fyle Technologies'}] From ed61812ca35371a7bc5bebd4f681e1df0cfc77e4 Mon Sep 17 00:00:00 2001 From: Hrishabh Tiwari <74908943+Hrishabh17@users.noreply.github.com> Date: Thu, 22 Feb 2024 19:51:49 +0530 Subject: [PATCH 5/5] Add unit tests (#99) * T1: Added Unit Test for journal_entry and purchase_invoice models * T2: Added/Modified test cases for business_central exceptions, models and views * Revert "Merged master to add-unit-tests" This reverts commit 2ce3bb1613b0f02fe8ead5983762a862a2b59786, reversing changes made to 7633c4fd02dc41da1dbcf242e3ccdd65c547960a. * Test cases for business_central, fyle, workspaces * lint fix --- apps/fyle/helpers.py | 6 + tests/test_business_central/conftest.py | 74 ++- tests/test_business_central/fixtures.py | 42 +- tests/test_business_central/test_helpers.py | 540 ++++++++++++++++++++ tests/test_business_central/test_models.py | 167 +++++- tests/test_business_central/test_queues.py | 92 ++++ tests/test_business_central/test_tasks.py | 257 ++++++++++ tests/test_business_central/test_utils.py | 145 ++++++ tests/test_business_central/test_views.py | 5 +- tests/test_fyle/conftest.py | 1 + tests/test_fyle/fixtures.py | 52 ++ tests/test_fyle/test_exceptions.py | 53 ++ tests/test_fyle/test_helpers.py | 141 +++++ tests/test_workspaces/test_helpers.py | 111 ++++ tests/test_workspaces/test_tasks.py | 22 + 15 files changed, 1698 insertions(+), 10 deletions(-) create mode 100644 tests/test_business_central/test_helpers.py create mode 100644 tests/test_business_central/test_queues.py create mode 100644 tests/test_business_central/test_tasks.py create mode 100644 tests/test_business_central/test_utils.py create mode 100644 tests/test_fyle/conftest.py create mode 100644 tests/test_fyle/test_exceptions.py create mode 100644 tests/test_fyle/test_helpers.py create mode 100644 tests/test_workspaces/test_helpers.py create mode 100644 tests/test_workspaces/test_tasks.py diff --git a/apps/fyle/helpers.py b/apps/fyle/helpers.py index 03b8692..41bd2a8 100644 --- a/apps/fyle/helpers.py +++ b/apps/fyle/helpers.py @@ -235,3 +235,9 @@ def get_fyle_orgs(refresh_token: str, cluster_domain: str): api_url = '{0}/api/orgs/'.format(cluster_domain) return get_request(api_url, {}, refresh_token) + + +def sync_dimensions(fyle_credentials: FyleCredential) -> None: + platform = PlatformConnector(fyle_credentials) + + platform.import_fyle_dimensions() diff --git a/tests/test_business_central/conftest.py b/tests/test_business_central/conftest.py index ee0622d..d050dc6 100644 --- a/tests/test_business_central/conftest.py +++ b/tests/test_business_central/conftest.py @@ -1,17 +1,25 @@ import pytest from apps.accounting_exports.models import AccountingExport -from apps.workspaces.models import AdvancedSetting, ExportSetting +from apps.workspaces.models import ( + AdvancedSetting, + ExportSetting, + Workspace, + ImportSetting, + BusinessCentralCredentials +) from apps.fyle.models import Expense from fyle_accounting_mappings.models import ( ExpenseAttribute, DestinationAttribute, EmployeeMapping, CategoryMapping, - Mapping + Mapping, + MappingSetting, + ExpenseField ) from apps.business_central.models import JournalEntry, JournalEntryLineItems from apps.business_central.models import PurchaseInvoice, PurchaseInvoiceLineitems -from apps.workspaces.models import Workspace +from apps.business_central.utils import BusinessCentralConnector from .fixtures import data @@ -255,3 +263,63 @@ def create_mapping_object( ) return mapping_object + + +@pytest.fixture() +@pytest.mark.django_db(databases=['default']) +def create_mapping_settings( + create_temp_workspace, + create_expense_attribute +): + workspace_id = 1 + + ImportSetting.objects.create( + workspace=Workspace.objects.get(id=workspace_id), + import_categories=False, + import_vendors_as_merchants=False, + ) + + ExpenseField.create_or_update_expense_fields( + attributes = data["expense_fields"], + fields_included = data["included_fields"], + workspace_id = workspace_id + ) + + attribute = data['included_fields'][0] + + MappingSetting.objects.update_or_create( + source_field = attribute, + destination_field = 'LOCATION', + import_to_fyle = False, + is_custom = False, + source_placeholder = attribute, + expense_field = ExpenseField.objects.filter( + workspace_id=workspace_id, + attribute_type=attribute + ).first(), + workspace_id=workspace_id + ) + + +@pytest.fixture() +@pytest.mark.django_db(databases=['default']) +def create_business_central_connection( + mocker, + create_temp_workspace, + add_business_central_creds +): + workspace_id = 1 + + business_central_creds = BusinessCentralCredentials.objects.filter(workspace_id=workspace_id).first() + business_central_creds.workspace.business_central_company_id = "Business_Company_Id" + business_central_creds.save() + + dynamic_connection_mock = mocker.patch('apps.business_central.utils.Dynamics') + dynamic_connection_mock.return_value.refresh_token = 'Dummy_Token' + + business_central_connection = BusinessCentralConnector( + credentials_object=business_central_creds, + workspace_id=workspace_id + ) + + return business_central_connection diff --git a/tests/test_business_central/fixtures.py b/tests/test_business_central/fixtures.py index 48701c8..bac80e0 100644 --- a/tests/test_business_central/fixtures.py +++ b/tests/test_business_central/fixtures.py @@ -38,7 +38,7 @@ }, "advanced_setting": { "workspace_id": 1, - "expense_memo_structure": ["Field1", "Field2", "Field3"], + "expense_memo_structure": ["Field1", "Field2", "category", "employee_email"], "schedule_is_enabled": True, "start_datetime": "2024-02-15T08:00:00Z", "schedule_id": "schedule123", @@ -143,5 +143,45 @@ 'paid_on_qbo':False, 'payment_number':'P/2022/05/R/7' } + ], + "expense_fields": [ + { + 'field_name': 'EMPLOYEE', + 'id': 1, + 'is_enabled': True + }, + { + 'field_name': 'CATEGORY', + 'id': 2, + 'is_enabled': True + }, + { + 'field_name': 'VENDOR', + 'id': 3, + 'is_enabled': True + }, + { + 'field_name': 'PROJECT', + 'id': 4, + 'is_enabled': True + }, + { + 'field_name': 'COST_CENTER', + 'id': 5, + 'is_enabled': True + }, + { + 'field_name': 'LOCATION', + 'id': 6, + 'is_enabled': True + } + ], + "included_fields": [ + 'EMPLOYEE', + 'CATEGORY', + 'VENDOR', + 'PROJECT', + 'COST_CENTER', + 'LOCATION' ] } diff --git a/tests/test_business_central/test_helpers.py b/tests/test_business_central/test_helpers.py new file mode 100644 index 0000000..5610d2a --- /dev/null +++ b/tests/test_business_central/test_helpers.py @@ -0,0 +1,540 @@ +import pytest +from datetime import datetime, timedelta, timezone + +from apps.business_central.exports.helpers import ( + get_employee_expense_attribute, + get_filtered_mapping, + __validate_category_mapping, + __validate_employee_mapping, + validate_accounting_export, + resolve_errors_for_exported_accounting_export, + load_attachments +) + +from apps.business_central.helpers import ( + check_interval_and_sync_dimension, + sync_dimensions +) + +from fyle_accounting_mappings.models import ( + CategoryMapping, + EmployeeMapping, + ExpenseAttribute, + Mapping +) + +from apps.accounting_exports.models import AccountingExport, Error +from apps.business_central.utils import BusinessCentralConnector, BusinessCentralCredentials +from apps.fyle.models import Expense +from apps.workspaces.models import ExportSetting, Workspace + +from ms_business_central_api.exceptions import BulkError + + +def test_get_employee_expense_attribute( + db, + mocker, + create_temp_workspace, + create_expense_attribute +): + workspace_id = 1 + value = 'ashwin.t@fyle.in' + + expense_attribute = ExpenseAttribute.objects.filter(workspace_id=workspace_id).first() + + expense_attribute_response = get_employee_expense_attribute(value=value, workspace_id=workspace_id) + + assert expense_attribute_response.value == expense_attribute.value + assert expense_attribute_response.workspace_id == expense_attribute.workspace_id + assert expense_attribute_response.attribute_type == expense_attribute.attribute_type + assert expense_attribute_response.display_name == expense_attribute.display_name + + +def test_resolve_errors_for_exported_accounting_export( + db, + create_temp_workspace, + create_export_settings, + create_accounting_export_expenses, +): + workspace_id = 1 + accounting_export = AccountingExport.objects.filter(workspace_id = workspace_id).first() + expense_attribute = ExpenseAttribute.objects.filter(workspace_id=workspace_id).first() + + Error.objects.create( + workspace_id=workspace_id, + type='EMPLOYEE_MAPPING', + accounting_export=accounting_export, + expense_attribute=expense_attribute, + is_resolved=False, + error_title='Employee Mapping error', + error_detail='Employee Mapping error detail' + ) + + resolve_errors_for_exported_accounting_export(accounting_export=accounting_export) + + error = Error.objects.filter(workspace_id=workspace_id).first() + + assert error.is_resolved == True + assert error.error_title == 'Employee Mapping error' + assert error.error_detail == 'Employee Mapping error detail' + assert error.type == 'EMPLOYEE_MAPPING' + + +def test_load_attachments_1( + db, + mocker, + create_temp_workspace, + add_business_central_creds, + add_fyle_credentials, + create_export_settings, + create_accounting_export_expenses +): + workspace_id = 1 + + expense = Expense.objects.filter(workspace_id=workspace_id).first() + accounting_export = AccountingExport.objects.filter(workspace_id=workspace_id).first() + + business_central_creds = BusinessCentralCredentials.objects.filter(workspace_id=workspace_id).first() + business_central_creds.workspace.business_central_company_id = "Business_Company_Id" + business_central_creds.save() + + dynamic_connection_mock = mocker.patch('dynamics.core.client.Dynamics') + dynamic_connection_mock.return_value.refresh_token = 'Dummy_Token' + platform_mock = mocker.patch('fyle_integrations_platform_connector.PlatformConnector') + mock_generate_file_urls = mocker.patch.object( + platform_mock.return_value.files, + 'bulk_generate_file_urls', + return_value=[ + { + 'id':1, + 'data':'dummy' + } + ] + ) + + business_central_connection = BusinessCentralConnector( + credentials_object=business_central_creds, + workspace_id=workspace_id + ) + + mock_post_attachments = mocker.patch.object(business_central_connection, 'post_attachments') + + load_attachments( + business_central_connection=business_central_connection, + ref_id="Ref_Id", + ref_type="Ref_Type", + expense=expense, + accounting_export=accounting_export + ) + + assert mock_post_attachments.call_count == 0 + assert mock_generate_file_urls.call_count == 0 + + +def test_load_attachments_2( + db, + mocker, + create_temp_workspace, + add_business_central_creds, + add_fyle_credentials, + create_export_settings, + create_accounting_export_expenses +): + workspace_id = 1 + + accounting_export = AccountingExport.objects.filter(workspace_id=workspace_id).first() + expense = Expense.objects.filter(workspace_id=workspace_id).first() + + expense.file_ids = [1, 2, 3] + expense.save() + + business_central_creds = BusinessCentralCredentials.objects.filter(workspace_id=workspace_id).first() + business_central_creds.workspace.business_central_company_id = "Business_Company_Id" + business_central_creds.save() + + dynamic_connection_mock = mocker.patch('apps.business_central.utils.Dynamics') + dynamic_connection_mock.return_value.refresh_token = 'Dummy_Token' + platform_mock = mocker.patch('apps.business_central.exports.helpers.PlatformConnector') + mock_generate_file_urls = mocker.patch.object( + platform_mock.return_value.files, + 'bulk_generate_file_urls', + return_value=[ + { + 'id':1, + 'data':'dummy' + } + ] + ) + + business_central_connection = BusinessCentralConnector( + credentials_object=business_central_creds, + workspace_id=workspace_id + ) + + assert business_central_creds.refresh_token == 'Dummy_Token' + + mock_post_attachments = mocker.patch.object(business_central_connection, 'post_attachments') + + load_attachments( + business_central_connection=business_central_connection, + ref_id="Ref_Id", + ref_type="Ref_Type", + expense=expense, + accounting_export=accounting_export + ) + + assert mock_post_attachments.call_count == 1 + assert mock_generate_file_urls.call_count == 1 + + +def test_load_attachments_3( + db, + mocker, + create_temp_workspace, + add_business_central_creds, + add_fyle_credentials, + create_export_settings, + create_accounting_export_expenses +): + workspace_id = 1 + + expense = Expense.objects.filter(workspace_id=workspace_id).first() + accounting_export = AccountingExport.objects.filter(workspace_id=workspace_id).first() + + business_central_creds = BusinessCentralCredentials.objects.filter(workspace_id=workspace_id).first() + business_central_creds.workspace.business_central_company_id = "Business_Company_Id" + business_central_creds.save() + + dynamic_connection_mock = mocker.patch('apps.business_central.utils.Dynamics') + dynamic_connection_mock.return_value.refresh_token = 'Dummy_Token' + platform_mock = mocker.patch( + 'apps.business_central.exports.helpers.PlatformConnector', + side_effect=Exception('Error') + ) + + business_central_connection = BusinessCentralConnector( + credentials_object=business_central_creds, + workspace_id=workspace_id + ) + + assert business_central_creds.refresh_token == 'Dummy_Token' + + with pytest.raises(Exception) as e: + load_attachments( + business_central_connection=business_central_connection, + ref_id="Ref_Id", + ref_type="Ref_Type", + expense=expense, + accounting_export=accounting_export + ) + + assert str(e.value) == 'Error' + + assert platform_mock.call_count == 1 + + +def test_get_filtered_mapping( + db, + create_temp_workspace, + create_mapping_object +): + workspace_id = 1 + mapping = Mapping.objects.filter(workspace_id=workspace_id).first() + + filtered_mapping = get_filtered_mapping( + source_field="EMPLOYEE", + destination_type="EMPLOYEE", + workspace_id=workspace_id, + source_id="source123", + source_value="ashwin.t@fyle.in" + ) + + assert filtered_mapping.source_type == mapping.source_type + assert filtered_mapping.destination_type == mapping.destination_type + assert filtered_mapping.workspace_id == mapping.workspace_id + assert filtered_mapping.source.source_id == mapping.source.source_id + assert filtered_mapping.source.value == mapping.source.value + + +def test_validate_employee_mapping_1( + db, + create_temp_workspace, + create_employee_mapping_with_employee, + create_export_settings, + create_accounting_export_expenses +): + workspace_id = 1 + accounting_export = AccountingExport.objects.filter(workspace_id=workspace_id).first() + export_settings = ExportSetting.objects.filter(workspace_id=workspace_id).first() + + bulk_errors = __validate_employee_mapping( + accounting_export=accounting_export, + export_settings=export_settings + ) + + employee_mapping = EmployeeMapping.objects.filter(workspace_id=workspace_id).first() + + assert employee_mapping.source_employee.source_id == 'source123' + assert len(bulk_errors) == 0 + + +def test_validate_employee_mapping_2( + db, + create_temp_workspace, + create_export_settings, + create_accounting_export_expenses +): + workspace_id = 1 + accounting_export = AccountingExport.objects.filter(workspace_id=workspace_id).first() + export_settings = ExportSetting.objects.filter(workspace_id=workspace_id).first() + + bulk_errors = __validate_employee_mapping( + accounting_export=accounting_export, + export_settings=export_settings + ) + + assert len(bulk_errors) == 1 + assert bulk_errors[0]['value'] == 'ashwin.t@fyle.in' + assert bulk_errors[0]['type'] == 'Employee Mapping' + assert bulk_errors[0]['message'] == 'Employee Mapping not found' + + +def test_validate_employee_mapping_3( + db, + create_temp_workspace, + create_export_settings, + create_accounting_export_expenses, + create_employee_mapping_with_employee +): + workspace_id = 1 + accounting_export = AccountingExport.objects.filter(workspace_id=workspace_id).first() + export_settings = ExportSetting.objects.filter(workspace_id=workspace_id).first() + employee_mapping = EmployeeMapping.objects.filter(workspace_id=workspace_id).first() + + employee_mapping.destination_employee = None + employee_mapping.save() + + bulk_errors = __validate_employee_mapping( + accounting_export=accounting_export, + export_settings=export_settings + ) + + error = Error.objects.filter(workspace_id=workspace_id).first() + + assert len(bulk_errors) == 1 + assert bulk_errors[0]['value'] == 'ashwin.t@fyle.in' + assert bulk_errors[0]['type'] == 'Employee Mapping' + assert bulk_errors[0]['message'] == 'Employee Mapping not found' + + assert error.type == 'EMPLOYEE_MAPPING' + assert error.error_detail == 'Employee mapping is missing' + assert error.is_resolved == False + + +def test_validate_employee_mapping_4( + db, + create_temp_workspace, + create_export_settings, + create_accounting_export_expenses, + create_employee_mapping_with_vendor +): + workspace_id = 1 + accounting_export = AccountingExport.objects.filter(workspace_id=workspace_id).first() + export_settings = ExportSetting.objects.filter(workspace_id=workspace_id).first() + + export_settings.employee_field_mapping = 'VENDOR' + export_settings.save() + + bulk_errors = __validate_employee_mapping( + accounting_export=accounting_export, + export_settings=export_settings + ) + + employee_mapping = EmployeeMapping.objects.filter(workspace_id=workspace_id).first() + + assert employee_mapping.source_employee.source_id == 'source123' + assert len(bulk_errors) == 0 + + +def test_validate_category_mapping_1( + db, + create_temp_workspace, + create_category_mapping, + create_export_settings, + create_accounting_export_expenses +): + workspace_id = 1 + accounting_export = AccountingExport.objects.filter(workspace_id=workspace_id).first() + + bulk_errors = __validate_category_mapping(accounting_export) + + category_mapping = CategoryMapping.objects.filter(workspace_id=workspace_id).first() + + assert category_mapping.source_category.attribute_type == 'CATEGORY' + assert len(bulk_errors) == 0 + + +def test_validate_category_mapping_2( + db, + create_temp_workspace, + create_export_settings, + create_accounting_export_expenses, + create_category_mapping +): + workspace_id = 1 + accounting_export = AccountingExport.objects.filter(workspace_id=workspace_id).first() + + category_mapping = CategoryMapping.objects.filter(workspace_id=workspace_id).first() + category_mapping.delete() + + assert accounting_export.expenses.first().category == 'Food' + + bulk_errors = __validate_category_mapping(accounting_export) + + error = Error.objects.filter(workspace_id=workspace_id).first() + + assert len(bulk_errors) == 1 + assert bulk_errors[0]['value'] == 'Food' + assert bulk_errors[0]['type'] == 'Category Mapping' + assert bulk_errors[0]['message'] == 'Category Mapping not found' + + assert error.type == 'CATEGORY_MAPPING' + assert error.error_detail == 'Category mapping is missing' + assert error.is_resolved == False + + +def test_validate_accounting_export( + db, + mocker, + create_temp_workspace, + create_export_settings, + create_accounting_export_expenses +): + workspace_id = 1 + accounting_export = AccountingExport.objects.filter(workspace_id=workspace_id).first() + export_settings = ExportSetting.objects.filter(workspace_id=workspace_id).first() + + mocker.patch( + 'apps.business_central.exports.helpers.__validate_employee_mapping', + return_value={ + 'row': 0, + 'id': 'Random Id', + 'error': 'Dummy Error', + 'type': 'Employee Mapping', + } + ) + mocker.patch( + 'apps.business_central.exports.helpers.__validate_category_mapping', + return_value={ + 'row': 0, + 'id': 'Random Id', + 'error': 'Dummy Error', + 'type': 'Category Mapping', + } + ) + + with pytest.raises(BulkError) as e: + validate_accounting_export( + accounting_export=accounting_export, + export_settings=export_settings + ) + + assert len(e.value.response) == 2 + assert e.value.response[0]['type'] == 'Employee Mapping' + assert e.value.response[1]['type'] == 'Category Mapping' + + +def test_check_interval_and_sync_dimension( + db, + mocker, + create_temp_workspace, + add_business_central_creds, +): + workspace_id = 1 + + business_central_creds = BusinessCentralCredentials.objects.get(workspace_id=workspace_id) + workspace = Workspace.objects.get(id=workspace_id) + + workspace.destination_synced_at = None + workspace.save() + + sync_dimensions_mock = mocker.patch('apps.business_central.helpers.sync_dimensions', return_value=None) + + return_value = check_interval_and_sync_dimension( + workspace=workspace, + business_central_credential=business_central_creds + ) + + assert sync_dimensions_mock.call_count == 1 + assert return_value == True + + workspace.destination_synced_at = datetime.now(timezone.utc) - timedelta(days=2) + workspace.save() + + return_value = check_interval_and_sync_dimension( + workspace=workspace, + business_central_credential=business_central_creds + ) + + assert sync_dimensions_mock.call_count == 2 + assert return_value == True + + workspace.destination_synced_at = datetime.now(timezone.utc) + workspace.save() + + return_value = check_interval_and_sync_dimension( + workspace=workspace, + business_central_credential=business_central_creds + ) + + assert sync_dimensions_mock.call_count == 2 + assert return_value == False + + +def test_sync_dimensions( + db, + mocker, + create_temp_workspace, + add_business_central_creds +): + workspace_id = 1 + + business_central_creds = BusinessCentralCredentials.objects.get(workspace_id=workspace_id) + business_central_connection_mock = mocker.patch('apps.business_central.utils.BusinessCentralConnector') + + mock_sync_companies = mocker.patch.object( + business_central_connection_mock.return_value, 'sync_companies' + ) + mock_sync_accounts = mocker.patch.object( + business_central_connection_mock.return_value, 'sync_accounts' + ) + mock_sync_vendors = mocker.patch.object( + business_central_connection_mock.return_value, 'sync_vendors' + ) + mock_sync_employees = mocker.patch.object( + business_central_connection_mock.return_value, 'sync_employees' + ) + mock_sync_locations = mocker.patch.object( + business_central_connection_mock.return_value, 'sync_locations' + ) + + sync_dimensions( + business_central_credential=business_central_creds, + workspace_id=workspace_id + ) + + assert mock_sync_companies.call_count == 1 + assert mock_sync_accounts.call_count == 1 + assert mock_sync_vendors.call_count == 1 + assert mock_sync_employees.call_count == 1 + assert mock_sync_locations.call_count == 1 + + business_central_connection_mock.return_value.sync_companies.side_effect = Exception('Error') + + with pytest.raises(Exception) as e: + sync_dimensions( + business_central_credential=business_central_creds, + workspace_id=workspace_id + ) + + assert str(e.value) == 'Error' diff --git a/tests/test_business_central/test_models.py b/tests/test_business_central/test_models.py index bb9100a..20cb463 100644 --- a/tests/test_business_central/test_models.py +++ b/tests/test_business_central/test_models.py @@ -6,9 +6,14 @@ PurchaseInvoice, PurchaseInvoiceLineitems ) +from fyle_accounting_mappings.models import ( + EmployeeMapping, + Mapping, + MappingSetting, + ExpenseAttribute +) from apps.workspaces.models import AdvancedSetting, ExportSetting -from apps.accounting_exports.models import AccountingExport -from fyle_accounting_mappings.models import EmployeeMapping +from apps.accounting_exports.models import AccountingExport, Expense from apps.business_central.exports.accounting_export import AccountingDataExporter @@ -324,3 +329,161 @@ def test_accounting_data_exporter_4( assert mock_body_model.create_or_update_object.call_count == 0 assert mock_lineitem_model.create_or_update_object.call_count == 0 assert mock_post.call_count == 0 + + +def test_base_model_get_invoice_date( + db, + create_temp_workspace, + create_journal_entry +): + workspace_id = 1 + + accounting_export = AccountingExport.objects.filter(workspace_id=workspace_id).first() + base_model = JournalEntry + + accounting_export.description = {"spent_at": "2023-04-01T00:00:00"} + return_value = base_model.get_invoice_date(accounting_export) + assert return_value == "2023-04-01T00:00:00" + + accounting_export = AccountingExport.objects.filter(workspace_id=workspace_id).first() + accounting_export.description = {"approved_at": "2023-04-01T00:00:00"} + accounting_export.save() + return_value = base_model.get_invoice_date(accounting_export=accounting_export) + assert return_value == "2023-04-01T00:00:00" + + accounting_export.description = {"verified_at": "2023-04-01T00:00:00"} + return_value = base_model.get_invoice_date(accounting_export=accounting_export) + assert return_value == "2023-04-01T00:00:00" + + accounting_export.description = {"last_spent_at": "2023-04-01T00:00:00"} + return_value = base_model.get_invoice_date(accounting_export=accounting_export) + assert return_value == "2023-04-01T00:00:00" + + accounting_export.description = {"posted_at": "2023-04-01T00:00:00"} + return_value = base_model.get_invoice_date(accounting_export=accounting_export) + assert return_value == "2023-04-01T00:00:00" + + +def test_base_model_get_location_id_1( + db, + create_mapping_object, + create_export_settings, + create_accounting_export_expenses, + create_mapping_settings +): + workspace_id = 1 + base_model = JournalEntry + + accounting_export = AccountingExport.objects.filter(workspace_id=workspace_id).first() + expenses = Expense.objects.filter(workspace_id=workspace_id).first() + + location_id = base_model.get_location_id(accounting_export, expenses) + + assert location_id is None + + +def test_base_model_get_location_id_2( + db, + create_mapping_object, + create_export_settings, + create_accounting_export_expenses, + create_mapping_settings +): + workspace_id = 1 + base_model = JournalEntry + + accounting_export = AccountingExport.objects.filter(workspace_id=workspace_id).first() + expenses = Expense.objects.filter(workspace_id=workspace_id).first() + expense_attribute = ExpenseAttribute.objects.filter(workspace_id=workspace_id).first() + expenses.project = 'ashwin.t@fyle.in' + expenses.save() + + mapping_setting = MappingSetting.objects.filter( + workspace_id=workspace_id, + destination_field='LOCATION' + ).first() + + mapping = Mapping.objects.filter( + workspace_id=workspace_id, + ).first() + + mapping_setting.source_field = 'PROJECT' + mapping_setting.save() + + mapping.source_type = 'PROJECT' + mapping.destination_type = 'LOCATION' + mapping.save() + + location_id = base_model.get_location_id(accounting_export, expenses) + assert location_id == mapping.destination.destination_id + + mapping_setting.source_field = 'COST_CENTER' + mapping_setting.save() + + mapping.source_type = 'COST_CENTER' + expense_attribute.value = 'Marketing' + expense_attribute.save() + mapping.source = expense_attribute + mapping.save() + + location_id = base_model.get_location_id(accounting_export, expenses) + assert location_id == mapping.destination.destination_id + + mapping_setting.source_field = 'CUSTOM' + mapping_setting.save() + + mapping.source_type = 'CUSTOM' + expense_attribute.attribute_type = 'CUSTOM' + expense_attribute.display_name = 'CUSTOM' + expense_attribute.value = 'Ashwin' + expense_attribute.save() + mapping.source = expense_attribute + mapping.save() + + expenses.custom_properties = { + 'CUSTOM': 'Ashwin' + } + expenses.save() + + location_id = base_model.get_location_id(accounting_export, expenses) + assert location_id == mapping.destination.destination_id + + +def test_get_expense_purpose( + db, + create_temp_workspace, + create_expense_objects, + add_advanced_settings +): + workspace_id = 1 + base_model = JournalEntry + category = 'Food' + + line_item = Expense.objects.filter(workspace_id=workspace_id).first() + advanced_settings = AdvancedSetting.objects.filter(workspace_id=workspace_id).first() + + return_value = base_model.get_expense_purpose(line_item, category, advanced_settings) + assert return_value == 'Food - ashwin.t@fyle.in' + + +def test_get_expense_comment( + db, + create_temp_workspace, + create_expense_objects, + add_advanced_settings, + add_fyle_credentials +): + workspace_id = 1 + base_model = JournalEntry + category = 'Food' + + line_item = Expense.objects.filter(workspace_id=workspace_id).first() + advanced_settings = AdvancedSetting.objects.filter(workspace_id=workspace_id).first() + + return_value = base_model.get_expense_comment( + workspace_id=workspace_id, + lineitem=line_item, + category=category, + advance_setting=advanced_settings + ) + assert return_value == 'Food - ashwin.t@fyle.in' diff --git a/tests/test_business_central/test_queues.py b/tests/test_business_central/test_queues.py new file mode 100644 index 0000000..61c6d59 --- /dev/null +++ b/tests/test_business_central/test_queues.py @@ -0,0 +1,92 @@ +from apps.business_central.exports.journal_entry.queues import ( + check_accounting_export_and_start_import + as + check_accounting_export_and_start_import_journal_entry +) +from apps.business_central.exports.purchase_invoice.queues import ( + check_accounting_export_and_start_import + as + check_accounting_export_and_start_import_purchase_invoice +) + + +def test_check_accounting_export_and_start_import_journal_entry( + db, + create_temp_workspace, + add_fyle_credentials, + create_export_settings, + create_accounting_export_expenses, + mocker +): + """ + Test check_accounting_export_and_start_import for journal entry + """ + accounting_export = create_accounting_export_expenses + + mocker.patch('apps.business_central.exports.journal_entry.tasks.create_journal_entry') + mocker.patch('apps.fyle.helpers.sync_dimensions') + mocker.patch('django_q.tasks.Chain.run') + + check_accounting_export_and_start_import_journal_entry( + accounting_export.workspace_id, + [accounting_export.id] + ) + + accounting_export.refresh_from_db() + + assert accounting_export.status == 'ENQUEUED' + assert accounting_export.type == 'JOURNAL_ENTRY' + + accounting_export.status = 'DONE' + accounting_export.save() + + check_accounting_export_and_start_import_journal_entry( + accounting_export.workspace_id, + [accounting_export.id] + ) + + accounting_export.refresh_from_db() + + assert accounting_export.status == 'ENQUEUED' + assert accounting_export.type == 'JOURNAL_ENTRY' + + +def test_check_accounting_export_and_start_import_purchase_invoice( + db, + create_temp_workspace, + add_fyle_credentials, + create_export_settings, + create_accounting_export_expenses, + mocker +): + """ + Test check_accounting_export_and_start_import for purchase invoice + """ + accounting_export = create_accounting_export_expenses + + mocker.patch('apps.business_central.exports.purchase_invoice.tasks.create_purchase_invoice') + mocker.patch('apps.fyle.helpers.sync_dimensions') + mocker.patch('django_q.tasks.Chain.run') + + check_accounting_export_and_start_import_purchase_invoice( + accounting_export.workspace_id, + [accounting_export.id] + ) + + accounting_export.refresh_from_db() + + assert accounting_export.status == 'ENQUEUED' + assert accounting_export.type == 'PURCHASE_INVOICE' + + accounting_export.status = 'COMPLETE' + accounting_export.save() + + check_accounting_export_and_start_import_purchase_invoice( + accounting_export.workspace_id, + [accounting_export.id] + ) + + accounting_export.refresh_from_db() + + assert accounting_export.status == 'COMPLETE' + assert accounting_export.type == 'PURCHASE_INVOICE' diff --git a/tests/test_business_central/test_tasks.py b/tests/test_business_central/test_tasks.py new file mode 100644 index 0000000..45bf26a --- /dev/null +++ b/tests/test_business_central/test_tasks.py @@ -0,0 +1,257 @@ +from apps.business_central.exports.journal_entry.tasks import ExportJournalEntry +from apps.business_central.exports.purchase_invoice.tasks import ExportPurchaseInvoice +from apps.business_central.exports.journal_entry.models import JournalEntry, JournalEntryLineItems +from apps.business_central.exports.purchase_invoice.models import PurchaseInvoice, PurchaseInvoiceLineitems +from apps.accounting_exports.models import AccountingExport +from apps.business_central.exports.journal_entry.tasks import create_journal_entry +from apps.business_central.exports.purchase_invoice.tasks import create_purchase_invoice + + +def test_trigger_export_journal_entry(db, mocker): + ''' + Test trigger_export method of ExportJournalEntry class + Just for coverage + ''' + + mocker.patch( + 'apps.business_central.exports.journal_entry.queues.check_accounting_export_and_start_import' + ) + + export_journal_entry = ExportJournalEntry() + export_journal_entry.trigger_export(1, [1]) + + assert True + + +def test_construct_journal_entry( + db, + mocker, + create_journal_line_items +): + ''' + Test __construct_journal_entry method of ExportJournalEntry class + ''' + workspace_id = 1 + journal_entry = JournalEntry.objects.filter(workspace_id=workspace_id).first() + lineitems = JournalEntryLineItems.objects.filter(workspace_id=workspace_id) + + export_journal_entry = ExportJournalEntry() + payload_list = export_journal_entry._ExportJournalEntry__construct_journal_entry(journal_entry, lineitems) + + assert len(payload_list) > 0 + payload = payload_list[0] + + assert payload['accountType'] == journal_entry.account_type + assert payload['accountNumber'] == journal_entry.account_id + assert payload['postingDate'] == journal_entry.invoice_date + assert payload['documentNumber'] == journal_entry.document_number + + +def test_post_export_journal_entry( + db, + mocker, + create_temp_workspace, + add_business_central_creds, + create_export_settings, + create_accounting_export_expenses, + create_journal_line_items +): + ''' + Test post_export method of ExportJournalEntry class + ''' + workspace_id = 1 + accounting_export = AccountingExport.objects.filter(workspace_id=workspace_id).first() + journal_entry = JournalEntry.objects.filter(workspace_id=workspace_id).first() + lineitems = JournalEntryLineItems.objects.filter(workspace_id=workspace_id) + + mocker_instance = mocker.MagicMock() + + mocker.patch( + 'apps.business_central.exports.journal_entry.tasks.BusinessCentralConnector', + return_value=mocker_instance + ) + + mocker_instance.bulk_post_journal_lineitems.return_value = { + "responses": [ + { + "body": { + "id": 12345 + } + }, + { + "body": { + "id": 67890 + } + } + ] + } + + mocker.patch( + 'apps.business_central.exports.helpers.load_attachments' + ) + + export_journal_entry = ExportJournalEntry() + response = export_journal_entry.post(accounting_export, journal_entry, lineitems) + + assert len(response["responses"]) == 2 + assert response['responses'][0]['body']['id'] == 12345 + assert response['responses'][1]['body']['id'] == 67890 + + +def test_create_journal_entry( + db, + mocker, + create_temp_workspace, + create_export_settings, + create_accounting_export_expenses, + add_accounting_export_summary +): + ''' + Test create_journal_entry method of ExportJournalEntry class + ''' + workspace_id = 1 + accounting_export = AccountingExport.objects.filter(workspace_id=workspace_id).first() + + mocker_instance = mocker.MagicMock() + mocker.patch( + 'apps.business_central.exports.journal_entry.tasks.ExportJournalEntry', + return_value=mocker_instance + ) + + mocker_instance.create_business_central_object.return_value = { + "id": 12345, + "dummy_key": "dummy_value" + } + + exported_journal_entry = create_journal_entry(accounting_export, True) + + assert exported_journal_entry['id'] == 12345 + assert exported_journal_entry['dummy_key'] == "dummy_value" + + +def test_trigger_export_purchase_invoice(db, mocker): + ''' + Test trigger_export method of ExportPurchaseInvoice class + Just for coverage + ''' + + mocker.patch( + 'apps.business_central.exports.purchase_invoice.queues.check_accounting_export_and_start_import' + ) + + export_purchase_invoice = ExportPurchaseInvoice() + export_purchase_invoice.trigger_export(1, [1]) + + assert True + + +def test_construct_purchase_invoice( + db, + mocker, + create_purchase_invoice_line_items +): + ''' + Test __construct_purchase_invoice method of ExportPurchaseInvoice class + ''' + + workspace_id = 1 + purchase_invoice = PurchaseInvoice.objects.filter(workspace_id=workspace_id).first() + lineitems = PurchaseInvoiceLineitems.objects.filter(workspace_id=workspace_id) + + export_purchase_invoice = ExportPurchaseInvoice() + payload, batch_payload = export_purchase_invoice.\ + _ExportPurchaseInvoice__construct_purchase_invoice( + purchase_invoice, + lineitems + ) + + assert payload['vendorNumber'] == purchase_invoice.vendor_number + assert payload['invoiceDate'] == purchase_invoice.invoice_date + + assert len(batch_payload) > 0 + assert batch_payload[0]['lineType'] == 'Account' + assert batch_payload[0]['lineObjectNumber'] == lineitems[0].accounts_payable_account_id + assert batch_payload[0]['unitCost'] == lineitems[0].amount + + +def test_post_export_purchase_invoice( + db, + mocker, + create_temp_workspace, + add_business_central_creds, + create_export_settings, + create_accounting_export_expenses, + create_purchase_invoice, + create_purchase_invoice_line_items +): + ''' + Test post_export method of ExportPurchaseInvoice class + ''' + workspace_id = 1 + accounting_export = AccountingExport.objects.filter(workspace_id=workspace_id).first() + purchase_invoice = PurchaseInvoice.objects.filter(workspace_id=workspace_id).first() + lineitems = PurchaseInvoiceLineitems.objects.filter(workspace_id=workspace_id) + + mocker_instance = mocker.MagicMock() + + mocker.patch( + 'apps.business_central.exports.purchase_invoice.tasks.BusinessCentralConnector', + return_value=mocker_instance + ) + + mocker_instance.post_purchase_invoice.return_value = { + "purchase_invoice_response": { + "id": 12345 + } + } + + mocker.patch( + 'apps.business_central.exports.helpers.load_attachments' + ) + + export_purchase_invoice = ExportPurchaseInvoice() + response = export_purchase_invoice.post(accounting_export, purchase_invoice, lineitems) + + assert response['purchase_invoice_response']['id'] == 12345 + + mocker_instance.post_purchase_invoice.return_value = { + "purchase_invoice_response": { + "id": 67890 + } + } + + lineitems.location_id = 'dummy_location_id' + response = export_purchase_invoice.post(accounting_export, purchase_invoice, lineitems) + + assert response['purchase_invoice_response']['id'] == 67890 + + +def test_create_purchase_invoice( + db, + mocker, + create_temp_workspace, + create_export_settings, + create_accounting_export_expenses, + add_accounting_export_summary +): + ''' + Test create_purchase_invoice method of ExportPurchaseInvoice class + ''' + workspace_id = 1 + accounting_export = AccountingExport.objects.filter(workspace_id=workspace_id).first() + + mocker_instance = mocker.MagicMock() + mocker.patch( + 'apps.business_central.exports.purchase_invoice.tasks.ExportPurchaseInvoice', + return_value=mocker_instance + ) + + mocker_instance.create_business_central_object.return_value = { + "id": 12345, + "dummy_key": "dummy_value" + } + + exported_purchase_invoice = create_purchase_invoice(accounting_export, True) + + assert exported_purchase_invoice['id'] == 12345 + assert exported_purchase_invoice['dummy_key'] == "dummy_value" diff --git a/tests/test_business_central/test_utils.py b/tests/test_business_central/test_utils.py new file mode 100644 index 0000000..e38ee29 --- /dev/null +++ b/tests/test_business_central/test_utils.py @@ -0,0 +1,145 @@ +def test_post_attachments( + db, + mocker, + create_business_central_connection + +): + business_central_connection = create_business_central_connection + + mocker.patch.object( + business_central_connection.connection.attachments, + 'post', + return_value={ + 'id': 'Return_Attachment_Id', + 'name': 'Return_Attachment_Name', + } + ) + + mocker.patch.object( + business_central_connection.connection.attachments, + 'upload' + ) + + attachments = [ + { + 'id': 'Attachment_Id', + 'name': 'Attachment_Name', + 'content_type': 'image/png', + 'download_url': 'aHR0cHM6Ly9kb3dubG9hZHVybC5jb20=' + } + ] + + ref_type = 'Expense' + ref_id = 'Expense_Id' + + response = business_central_connection.post_attachments( + ref_id=ref_id, + ref_type=ref_type, + attachments=attachments + ) + + assert len(response) == 2 + assert response['id'] == 'Return_Attachment_Id' + assert response['name'] == 'Return_Attachment_Name' + + +def test_post_purchase_invoice( + db, + mocker, + create_business_central_connection +): + business_central_connection = create_business_central_connection + + mocker.patch.object( + business_central_connection.connection.purchase_invoices, + 'post', + return_value={ + 'id': 'Return_Purchase_Invoice_Id', + } + ) + + mocker.patch.object( + business_central_connection.connection.purchase_invoice_line_items, + 'bulk_post', + return_value=[ + { + 'id': 'Invoice_Line_Item_Id', + }, + { + 'id': 'Invoice_Line_Item_Id_2', + } + ] + ) + + purchase_invoice_payload = { + 'id': 'Purchase_Invoice_Id', + 'name': 'Purchase_Invoice_Name', + } + + purchase_invoice_lineitem_payload = [ + { + 'id': 'Purchase_Invoice_Line_Item_Id', + 'name': 'Purchase_Invoice_Line_Item_Name', + }, + { + 'id': 'Purchase_Invoice_Line_Item_Id_2', + 'name': 'Purchase_Invoice_Line_Item_Name_2', + } + ] + + response = business_central_connection.post_purchase_invoice( + purchase_invoice_payload=purchase_invoice_payload, + purchase_invoice_lineitem_payload=purchase_invoice_lineitem_payload + ) + + assert len(response) == 2 + assert response['purchase_invoice_response']['id'] == 'Return_Purchase_Invoice_Id' + assert response['bulk_post_response'][0]['id'] == 'Invoice_Line_Item_Id' + assert response['bulk_post_response'][1]['id'] == 'Invoice_Line_Item_Id_2' + + +def test_bulk_post_journal_lineitems( + db, + mocker, + create_business_central_connection, + create_export_settings +): + business_central_connection = create_business_central_connection + + mocker.patch.object( + business_central_connection.connection.journal_line_items, + 'bulk_post', + return_value=[ + { + 'id': 'Journal_Line_Item_Id', + }, + { + 'id': 'Journal_Line_Item_Id_2', + } + ] + ) + + payload = [ + { + 'id': 'Journal_Line_Item_Id', + 'name': 'Journal_Line_Item_Name', + }, + { + 'id': 'Journal_Line_Item_Id_2', + 'name': 'Journal_Line_Item_Name_2', + } + ] + + accounting_export = { + 'id': 'Accounting_Export_Id', + 'name': 'Accounting_Export_Name', + } + + response = business_central_connection.bulk_post_journal_lineitems( + payload=payload, + accounting_export=accounting_export + ) + + assert len(response) == 2 + assert response[0]['id'] == 'Journal_Line_Item_Id' + assert response[1]['id'] == 'Journal_Line_Item_Id_2' diff --git a/tests/test_business_central/test_views.py b/tests/test_business_central/test_views.py index 87a0735..6964a5f 100644 --- a/tests/test_business_central/test_views.py +++ b/tests/test_business_central/test_views.py @@ -42,10 +42,7 @@ def test_sync_dimensions_1(api_client, test_connection, mocker, create_temp_work mocker.patch('apps.workspaces.models.BusinessCentralCredentials.objects.get', side_effect=Exception("Unexpected error")) with pytest.raises(Exception): - response = api_client.post(url) - - assert response.status_code == 500 - assert response.data['message'] == 'Something unexpected happened' + api_client.post(url) def test_business_central_fields(api_client, test_connection, create_temp_workspace, add_fyle_credentials, add_destination_attributes): diff --git a/tests/test_fyle/conftest.py b/tests/test_fyle/conftest.py new file mode 100644 index 0000000..df90441 --- /dev/null +++ b/tests/test_fyle/conftest.py @@ -0,0 +1 @@ +from tests.test_business_central.conftest import * # noqa \ No newline at end of file diff --git a/tests/test_fyle/fixtures.py b/tests/test_fyle/fixtures.py index 65db996..b7f1c3e 100644 --- a/tests/test_fyle/fixtures.py +++ b/tests/test_fyle/fixtures.py @@ -374,4 +374,56 @@ 'payment_number':'P/2022/05/R/7' } ], + "expense_filter_payload": [ + { + "condition": "employee_email", + "operator": "in", + "values": ["ashwinn.t@fyle.in", "admin3@fyleforleaf.in"], + "rank": 1, + "join_by": "AND", + "is_custom": False, + "custom_field_type": "SELECT", + "workspace_id": 4, + }, + { + "condition": "spent_at", + "operator": "isnull", + "values": ['2020-04-20 23:59:59+00'], + "rank": 1, + "join_by": None, + "is_custom": True, + "custom_field_type": "SELECT", + "workspace_id": 4, + }, + { + "condition": "some_field", + "operator": "isnull", + "values": ['True'], + "rank": 1, + "join_by": None, + "is_custom": True, + "custom_field_type": "SELECT", + "workspace_id": 4, + }, + { + "condition": "employee_id", + "operator": "not_in", + "values": ["12", "13"], + "rank": 2, + "join_by": "OR", + "is_custom": True, + "custom_field_type": "NUMBER", + "workspace_id": 4, + }, + { + "condition": "is_email_sent", + "operator": "not_in", + "values": ["True"], + "rank": 2, + "join_by": "AND", + "is_custom": True, + "custom_field_type": "BOOLEAN", + "workspace_id": 4, + }, + ], } diff --git a/tests/test_fyle/test_exceptions.py b/tests/test_fyle/test_exceptions.py new file mode 100644 index 0000000..9153a16 --- /dev/null +++ b/tests/test_fyle/test_exceptions.py @@ -0,0 +1,53 @@ +from apps.fyle.exceptions import handle_exceptions +from fyle.platform.exceptions import ( + NoPrivilegeError, + RetryException +) +from apps.workspaces.models import FyleCredential +from apps.accounting_exports.models import AccountingExport + + +def test_handle_exceptions( + db, + mocker, + create_temp_workspace, + create_export_settings, + create_accounting_export_expenses, +): + workspace_id = 1 + accounting_export = AccountingExport.objects.filter(workspace_id=workspace_id).first() + + @handle_exceptions + def test_func(workspace_id, accounting_export): + raise FyleCredential.DoesNotExist('Fyle credentials not found') + + test_func(1, accounting_export) + + assert accounting_export.status == 'FAILED' + assert accounting_export.detail == {'message': 'Fyle credentials do not exist in workspace'} + + @handle_exceptions + def test_func(workspace_id, accounting_export): + raise NoPrivilegeError('No privilege to access the resource') + + test_func(1, accounting_export) + + assert accounting_export.status == 'FAILED' + assert accounting_export.detail == {'message': 'Invalid Fyle Credentials / Admin is disabled'} + + @handle_exceptions + def test_func(workspace_id, accounting_export): + raise RetryException('Retry Exception') + + test_func(1, accounting_export) + + assert accounting_export.status == 'FATAL' + assert accounting_export.detail == {'message': 'Fyle Retry Exception occured'} + + @handle_exceptions + def test_func(workspace_id, accounting_export): + raise Exception('Random Exception') + + test_func(1, accounting_export) + + assert accounting_export.status == 'FATAL' diff --git a/tests/test_fyle/test_helpers.py b/tests/test_fyle/test_helpers.py new file mode 100644 index 0000000..6172ec2 --- /dev/null +++ b/tests/test_fyle/test_helpers.py @@ -0,0 +1,141 @@ +from requests import Response +from apps.fyle.helpers import ( + get_fyle_orgs, + get_request, + post_request, + construct_expense_filter_query, +) +from apps.fyle.models import ExpenseFilter +from tests.test_fyle.fixtures import fixtures as data + + +def test_get_fyle_orgs(mocker): + mocker.patch( + 'apps.fyle.helpers.get_request', + return_value={ + 'data': [ + { + 'id': 1, + 'name': 'Fyle Org 1' + } + ] + } + ) + + fyle_org = get_fyle_orgs( + refresh_token='refresh_token', + cluster_domain='cluster_domain' + ) + + assert fyle_org.get('data')[0].get('id') == 1 + assert fyle_org.get('data')[0].get('name') == 'Fyle Org 1' + + +def test_get_request(mocker): + mock_response = Response() + mock_response.status_code = 200 + mock_response._content = b'{"data": "1234"}' + + mocker.patch( + 'apps.fyle.helpers.requests.get', + return_value=mock_response + ) + + mocker.patch( + 'apps.fyle.helpers.get_access_token', + return_value='access_token' + ) + + response = get_request( + url='https://api.fyle.tech/api/v7/cluster/', + params={ + 'id': 1, + 'is_active': True + }, + refresh_token='refresh_token', + ) + + assert response.get('data') == "1234" + + mock_response.status_code = 400 + mock_response._content = b'{"error": "error"}' + + try: + response = get_request( + url='https://api.fyle.tech/api/v7/cluster/', + params={ + 'id': 1, + 'is_active': True + }, + refresh_token = 'refresh_token', + ) + except Exception as e: + assert str(e) == '{"error": "error"}' + + +def test_post_request(mocker): + mock_response = Response() + mock_response.status_code = 200 + mock_response._content = b'{"data": "1234"}' + + mocker.patch( + 'apps.fyle.helpers.requests.post', + return_value=mock_response + ) + + mocker.patch( + 'apps.fyle.helpers.get_access_token', + return_value='access_token' + ) + + response = post_request( + url='https://api.fyle.tech/api/v7/cluster/', + body={ + 'id': 1, + 'is_active': True + }, + refresh_token='refresh_token', + ) + + assert response.get('data') == "1234" + + mock_response.status_code = 400 + mock_response._content = b'{"error": "error"}' + + try: + response = post_request( + url='https://api.fyle.tech/api/v7/cluster/', + body={ + 'id': 1, + 'is_active': True + }, + refresh_token='refresh_token', + ) + except Exception as e: + assert str(e) == '{"error": "error"}' + + +def test_construct_expense_filter_query( + db, + mocker, +): + expense_payload_data = data["expense_filter_payload"] + + expense_payload_req = [] + + for expense_payload in expense_payload_data: + expense_filter = mocker.MagicMock(spec=ExpenseFilter) + expense_filter.condition = expense_payload.get('condition') + expense_filter.rank = expense_payload.get('rank') + expense_filter.join_by = expense_payload.get('join_by') + expense_filter.custom_field_type = expense_payload.get('custom_field_type') + expense_filter.is_custom = expense_payload.get('is_custom') + expense_filter.operator = expense_payload.get('operator') + expense_filter.values = expense_payload.get('values') + expense_filter.workspace_id = expense_payload.get('workspace_id') + + expense_payload_req.append(expense_filter) + + returned_filter = construct_expense_filter_query(expense_filters=expense_payload_req) + + assert str(returned_filter) == "(OR: ('custom_properties__some_field__isnull', True), ('custom_properties__some_field__exact', None), ('custom_properties__employee_id__not_in', [12, 13]), ('custom_properties__is_email_sent__not_in', False))" diff --git a/tests/test_workspaces/test_helpers.py b/tests/test_workspaces/test_helpers.py new file mode 100644 index 0000000..3964c99 --- /dev/null +++ b/tests/test_workspaces/test_helpers.py @@ -0,0 +1,111 @@ +from apps.workspaces.helpers import ( + generate_token, + generate_business_central_refresh_token, + connect_business_central +) +from dynamics.exceptions.dynamics_exceptions import ( + InternalServerError, + InvalidTokenError +) +from apps.workspaces.models import BusinessCentralCredentials +from requests import Response + + +def test_generate_token(mocker): + auth_code = "random_auth_code" + redirect_uri = "https://redirect_uri.com" + + mock_response = Response() + mock_response.status_code = 200 + mock_response._content = b'{"refresh_token": "token123"}' + + mocker.patch( + "requests.post", + return_value=mock_response + ) + + response = generate_token(auth_code, redirect_uri) + assert response.status_code == 200 + assert response.text == '{"refresh_token": "token123"}' + + +def test_generate_business_central_refresh_token_success(mocker): + auth_code = "random_auth_code" + redirect_uri = "https://redirect_uri.com" + + mock_response = Response() + mock_response.status_code = 200 + mock_response._content = b'{"refresh_token": "token123"}' + + mocker.patch( + "apps.workspaces.helpers.generate_token", + return_value=mock_response + ) + + response = generate_business_central_refresh_token(auth_code, redirect_uri) + assert response == "token123" + + +def test_generate_business_central_refresh_token_failure(mocker): + auth_code = "random_auth_code" + redirect_uri = "https://redirect_uri.com" + + mock_response = Response() + mock_response.status_code = 500 + mock_response._content = b'{"error": "Something is wrong"}' + + with mocker.patch( + "apps.workspaces.helpers.generate_token", + return_value=mock_response + ): + try: + generate_business_central_refresh_token(auth_code, redirect_uri) + except InternalServerError as e: + assert str(e) == "'Internal server error'" + + mock_response.status_code = 400 + + with mocker.patch( + "apps.workspaces.helpers.generate_token", + return_value=mock_response + ): + try: + generate_business_central_refresh_token(auth_code, redirect_uri) + except InvalidTokenError as e: + assert str(e) == "'Wrong client secret or/and refresh token'" + + +def test_connect_business_central( + db, + mocker, + create_temp_workspace, + add_business_central_creds, +): + auth_code = "random_auth_code" + redirect_uri = "https://redirect_uri.com" + workspace_id = 1 + + mocker.patch( + "apps.workspaces.helpers.generate_business_central_refresh_token", + return_value="token123" + ) + + business_cred = connect_business_central( + authorization_code=auth_code, + redirect_uri=redirect_uri, + workspace_id=workspace_id + ) + assert business_cred.refresh_token == "token123" + assert business_cred.is_expired == False + + business_credentials = BusinessCentralCredentials.objects.get(workspace_id=workspace_id) + business_credentials.delete() + + business_cred = connect_business_central( + authorization_code=auth_code, + redirect_uri=redirect_uri, + workspace_id=workspace_id + ) + + assert business_cred.refresh_token == "token123" + assert business_cred.is_expired == False diff --git a/tests/test_workspaces/test_tasks.py b/tests/test_workspaces/test_tasks.py new file mode 100644 index 0000000..e1a79c4 --- /dev/null +++ b/tests/test_workspaces/test_tasks.py @@ -0,0 +1,22 @@ +from apps.workspaces.tasks import ( + async_update_fyle_credentials +) +from apps.workspaces.models import FyleCredential + + +def test_async_update_fyle_credentials( + db, + mocker, + create_temp_workspace, + add_fyle_credentials +): + workspace_id = 1 + org_id = "riseabovehate1" + + async_update_fyle_credentials( + fyle_org_id=org_id, + refresh_token="refresh_token" + ) + + fyle_credentials = FyleCredential.objects.get(workspace_id=workspace_id) + assert fyle_credentials.refresh_token == "refresh_token"