diff --git a/backend/benefit/applications/models.py b/backend/benefit/applications/models.py index 55f5c90abb..25faf5b1da 100755 --- a/backend/benefit/applications/models.py +++ b/backend/benefit/applications/models.py @@ -399,6 +399,10 @@ def total_deminimis_amount(self): total += deminimis_aid.amount return total + @property + def contact_person(self): + return f"{self.company_contact_person_first_name} {self.company_contact_person_last_name}" + def get_log_entry_field(self, to_statuses, field_name): if ( log_entry := self.log_entries.filter(to_status__in=to_statuses) diff --git a/backend/benefit/applications/services/ahjo_authentication.py b/backend/benefit/applications/services/ahjo_authentication.py index 8bcde2dcf9..5a2f2347f7 100644 --- a/backend/benefit/applications/services/ahjo_authentication.py +++ b/backend/benefit/applications/services/ahjo_authentication.py @@ -4,7 +4,7 @@ import requests from django.conf import settings -from django.core.exceptions import ObjectDoesNotExist +from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist from applications.models import AhjoSetting @@ -17,8 +17,7 @@ class AhjoToken: class AhjoConnector: - def __init__(self, requests_module: requests.Session = requests) -> None: - self.requests_module: requests = requests_module + def __init__(self) -> None: self.token_url: str = settings.AHJO_TOKEN_URL self.client_id: str = settings.AHJO_CLIENT_ID self.client_secret: str = settings.AHJO_CLIENT_SECRET @@ -29,7 +28,7 @@ def __init__(self, requests_module: requests.Session = requests) -> None: self.headers: Dict[str, str] = { "Content-Type": "application/x-www-form-urlencoded", } - self.timout: int = 10 + self.timeout: int = 10 def is_configured(self) -> bool: """Check if all required config options are set""" @@ -56,7 +55,7 @@ def get_new_token(self, auth_code: str) -> AhjoToken: this is only used when getting the initial token or when the token has expired. """ if not auth_code: - raise Exception("No auth code") + raise ImproperlyConfigured("no auth code configured") payload = { "client_id": self.client_id, "client_secret": self.client_secret, @@ -72,7 +71,7 @@ def refresh_token(self) -> AhjoToken: """ token = self.get_token_from_db() if not token.refresh_token: - raise Exception("No refresh token") + raise ImproperlyConfigured("No refresh token configured") payload = { "client_id": self.client_id, @@ -85,8 +84,8 @@ def refresh_token(self) -> AhjoToken: def do_token_request(self, payload: Dict[str, str]) -> AhjoToken: # Make the POST request - response = self.requests_module.post( - self.token_url, headers=self.headers, data=payload, timeout=self.timout + response = requests.post( + self.token_url, headers=self.headers, data=payload, timeout=self.timeout ) # Check if the request was successful diff --git a/backend/benefit/applications/services/ahjo_integration.py b/backend/benefit/applications/services/ahjo_integration.py index 7bd2a6f46e..ad8debc1b7 100644 --- a/backend/benefit/applications/services/ahjo_integration.py +++ b/backend/benefit/applications/services/ahjo_integration.py @@ -1,5 +1,7 @@ +import json import logging import os +import uuid import zipfile from collections import defaultdict from dataclasses import dataclass @@ -10,12 +12,14 @@ import pdfkit import requests from django.conf import settings -from django.core.exceptions import ObjectDoesNotExist +from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist from django.db.models import QuerySet +from django.urls import reverse -from applications.enums import ApplicationStatus -from applications.models import AhjoSetting, Application +from applications.enums import AhjoStatus as AhjoStatusEnum, ApplicationStatus +from applications.models import AhjoSetting, AhjoStatus, Application from applications.services.ahjo_authentication import AhjoConnector +from applications.services.ahjo_payload import prepare_open_case_payload from applications.services.applications_csv_report import ApplicationsCsvService from companies.models import Company @@ -358,44 +362,102 @@ def export_application_batch(batch) -> bytes: return generate_zip(pdf_files) -def dummy_ahjo_request(): - """Dummy function for preliminary testing of Ahjo integration""" - ahjo_api_url = settings.AHJO_REST_API_URL +def get_token() -> str: + """Get the access token from Ahjo Service.""" try: ahjo_auth_code = AhjoSetting.objects.get(name="ahjo_code").data LOGGER.info(f"Retrieved auth code: {ahjo_auth_code}") + connector = AhjoConnector() + + if not connector.is_configured(): + LOGGER.warning("AHJO connector is not configured") + return + return connector.get_access_token(ahjo_auth_code["code"]) except ObjectDoesNotExist: LOGGER.error( "Error: Ahjo auth code not found in database. Please set the 'ahjo_code' setting." ) return - - connector = AhjoConnector(requests) - - if not connector.is_configured(): - LOGGER.warning("AHJO connector is not configured") - return - try: - ahjo_token = connector.get_access_token(ahjo_auth_code["code"]) except Exception as e: LOGGER.warning(f"Error retrieving access token: {e}") return - headers = { - "Authorization": f"Bearer {ahjo_token.access_token}", + + +def prepare_headers(access_token: str, application_uuid: uuid) -> dict: + """Prepare the headers for the Ahjo request.""" + url = reverse("ahjo_callback_url", kwargs={"uuid": str(application_uuid)}) + + return { + "Authorization": f"Bearer {access_token}", + "Accept": "application/hal+json", + "X-CallbackURL": f"{settings.API_BASE_URL}{url}", } - print(headers) + + +def get_application_for_ahjo(id: uuid) -> Optional[Application]: + """Get the first accepted application.""" + application = ( + Application.objects.filter(pk=id, status=ApplicationStatus.ACCEPTED) + .prefetch_related("attachments", "calculation", "company") + .first() + ) + if not application: + raise ObjectDoesNotExist("No applications found for Ahjo request.") + # Check that the handler has an ad_username set, if not, ImproperlyConfigured + if not application.calculation.handler.ad_username: + raise ImproperlyConfigured( + "No ad_username set for the handler for Ahjo request." + ) + return application + + +def create_status_for_application(application: Application): + """Create a new AhjoStatus for the application.""" + AhjoStatus.objects.create( + application=application, status=AhjoStatusEnum.REQUEST_TO_OPEN_CASE_SENT + ) + + +def do_ahjo_request_with_json_payload( + url: str, headers: dict, data: dict, application: Application, timeout: int = 10 +): + headers["Content-Type"] = "application/json" + + json_data = json.dumps(data) + try: - response = requests.get( - f"{ahjo_api_url}/cases", headers=headers, timeout=connector.timout + response = requests.post( + f"{url}/cases", headers=headers, timeout=timeout, data=json_data ) response.raise_for_status() - print(response.json()) + + if response.ok: + create_status_for_application(application) + LOGGER.info( + f"Open case for application {application.id} Request to Ahjo was successful." + ) + except requests.exceptions.HTTPError as e: # Handle the HTTP error - LOGGER.error(f"HTTP error occurred: {e}") + LOGGER.error(f"HTTP error occurred while sending request to Ahjo: {e}") except requests.exceptions.RequestException as e: # Handle the network error - LOGGER.errror(f"Network error occurred: {e}") + LOGGER.error(f"Network error occurred while sending request to Ahjo: {e}") except Exception as e: # Handle any other error - LOGGER.error(f"Error occurred: {e}") + LOGGER.error(f"Error occurred while sending request to Ahjo: {e}") + + +def open_case_in_ahjo(application_id: uuid): + """Open a case in Ahjo.""" + try: + application = get_application_for_ahjo(application_id) + ahjo_api_url = settings.AHJO_REST_API_URL + ahjo_token = get_token() + headers = prepare_headers(ahjo_token.access_token, application.id) + data = prepare_open_case_payload(application) + do_ahjo_request_with_json_payload(ahjo_api_url, headers, data, application) + except ObjectDoesNotExist as e: + LOGGER.error(f"Object not found: {e}") + except ImproperlyConfigured as e: + LOGGER.error(f"Improperly configured: {e}") diff --git a/backend/benefit/applications/services/ahjo_payload.py b/backend/benefit/applications/services/ahjo_payload.py new file mode 100644 index 0000000000..2caf2c51a3 --- /dev/null +++ b/backend/benefit/applications/services/ahjo_payload.py @@ -0,0 +1,134 @@ +import uuid +from datetime import datetime +from typing import List, Union + +from django.conf import settings +from django.urls import reverse + +from applications.models import Application, Attachment +from common.utils import hash_file +from users.models import User + + +def _prepare_top_level_dict(application: Application, case_records: List[dict]) -> dict: + """Prepare the dictionary that is sent to Ahjo""" + application_date = application.created_at.isoformat() + application_year = application.created_at.year + title = f"Avustuksen myöntäminen, työllisyyspalvelut, \ +työnantajan Helsinki-lisä vuonna {application.created_at.year}, \ +työnantaja {application.company_name}" + + case_dict = { + "Title": title, + "Acquired": application_date, + "ClassificationCode": "02 05 01 00", + "ClassificationTitle": "Kunnan myöntämät avustukset", + "Language": "fi", + "PublicityClass": "Julkinen", + "InternalTitle": f"Avustuksen myöntäminen, työllisyyspalvelut, \ + työnantajan Helsinki-lisä vuonna {application_year}, \ + työnantaja {application.company_name}", + "Subjects": [ + {"Subject": "Helsinki-lisät", "Scheme": "hki-yhpa"}, + {"Subject": "kunnan myöntämät avustukset", "Scheme": "hki-yhpa"}, + {"Subject": "työnantajat", "Scheme": "hki-yhpa"}, + {"Subject": "työllisyydenhoito"}, + ], + "PersonalData": "Sisältää erityisiä henkilötietoja", + "Reference": application.application_number, + "Records": case_records, + "Agents": [ + { + "Role": "sender_initiator", + "CorporateName": application.company.name, + "ContactPerson": application.contact_person, + "Type": "ExterOnal", + "Email": application.company_contact_person_email, + "AddressStreet": application.company.street_address, + "AddressPostalCode": application.company.postcode, + "AddressCity": application.company.city, + } + ], + } + return case_dict + + +def _prepare_record_document_dict(attachment: Attachment) -> dict: + """Prepare a documents dict for a record""" + # If were running in mock mode, use the local file URI + file_url = reverse("ahjo_attachment_url", kwargs={"uuid": attachment.id}) + hash_value = hash_file(attachment.attachment_file) + return { + "FileName": f"{attachment.attachment_file.name}", + "FormatName": f"{attachment.content_type}", + "HashAlgorithm": "sha256", + "HashValue": hash_value, + "FileURI": f"{settings.API_BASE_URL}{file_url}", + } + + +def _prepare_record( + record_title: str, + record_type: str, + acquired: datetime, + reference: Union[int, uuid.UUID], + documents: List[dict], + handler: User, + publicity_class: str = "Salassa pidettävä", +): + """Prepare a single record dict for Ahjo.""" + + return { + "Title": record_title, + "Type": record_type, + "Acquired": acquired, + "PublicityClass": publicity_class, + "SecurityReasons": ["JulkL (621/1999) 24.1 § 25 k"], + "Language": "fi", + "PersonalData": "Sisältää erityisiä henkilötietoja", + "Reference": str(reference), + "Documents": documents, + "Agents": [ + { + "Role": "mainCreator", + "Name": f"{handler.last_name}, {handler.first_name}", + "ID": handler.ad_username, + } + ], + } + + +def _prepare_case_records(application: Application) -> List[dict]: + """Prepare the list of case records""" + case_records = [] + handler = application.calculation.handler + main_document_record = _prepare_record( + "Hakemus", + "hakemus", + application.created_at.isoformat(), + application.application_number, + [], # TODO Pdf version of the application goes here with prepare_record() + handler, + ) + + case_records.append(main_document_record) + + for attachment in application.attachments.all(): + document_record = _prepare_record( + "Hakemuksen Liite", + "liite", + attachment.created_at.isoformat(), + attachment.id, + [_prepare_record_document_dict(attachment)], + handler, + ) + case_records.append(document_record) + + return case_records + + +def prepare_open_case_payload(application: Application) -> dict: + "Prepare the complete dictionary payload that is sent to Ahjo" + case_records = _prepare_case_records(application) + payload = _prepare_top_level_dict(application, case_records) + return payload diff --git a/backend/benefit/applications/tests/conftest.py b/backend/benefit/applications/tests/conftest.py index 8e8ba1c98f..8c6aa1a981 100755 --- a/backend/benefit/applications/tests/conftest.py +++ b/backend/benefit/applications/tests/conftest.py @@ -9,6 +9,7 @@ from applications.enums import ApplicationStatus, BenefitType from applications.models import Application +from applications.services.ahjo_payload import prepare_open_case_payload from applications.services.applications_csv_report import ApplicationsCsvService from applications.tests.factories import ( ApplicationBatchFactory, @@ -257,6 +258,79 @@ def set_debug_to_false(settings): settings.DEBUG = False +@pytest.fixture() +def ahjo_payload_record(decided_application): + application = decided_application + + record_title = "Hakemus" + record_type = "hakemus" + acquired = application.created_at.isoformat() + reference = str(application.application_number) + documents = [] + agent = application.calculation.handler + publicity_class = "Salassa pidettävä" + + return { + "Title": record_title, + "Type": record_type, + "Acquired": acquired, + "PublicityClass": publicity_class, + "SecurityReasons": ["JulkL (621/1999) 24.1 § 25 k"], + "Language": "fi", + "PersonalData": "Sisältää erityisiä henkilötietoja", + "Reference": reference, + "Documents": documents, + "Agents": [ + { + "Role": "mainCreator", + "Name": f"{agent.last_name}, {agent.first_name}", + "ID": agent.ad_username, + } + ], + } + + +@pytest.fixture() +def ahjo_open_case_top_level_dict(decided_application): + application = decided_application + title = f"Avustuksen myöntäminen, työllisyyspalvelut, \ +työnantajan Helsinki-lisä vuonna {application.created_at.year}, \ +työnantaja {application.company_name}" + + return { + "Title": title, + "Acquired": application.created_at.isoformat(), + "ClassificationCode": "02 05 01 00", + "ClassificationTitle": "Kunnan myöntämät avustukset", + "Language": "fi", + "PublicityClass": "Julkinen", + "InternalTitle": f"Avustuksen myöntäminen, työllisyyspalvelut, \ + työnantajan Helsinki-lisä vuonna {application.created_at.year}, \ + työnantaja {application.company_name}", + "Subjects": [ + {"Subject": "Helsinki-lisät", "Scheme": "hki-yhpa"}, + {"Subject": "kunnan myöntämät avustukset", "Scheme": "hki-yhpa"}, + {"Subject": "työnantajat", "Scheme": "hki-yhpa"}, + {"Subject": "työllisyydenhoito"}, + ], + "PersonalData": "Sisältää erityisiä henkilötietoja", + "Reference": application.application_number, + "Records": [], + "Agents": [ + { + "Role": "sender_initiator", + "CorporateName": application.company.name, + "ContactPerson": application.contact_person, + "Type": "ExterOnal", + "Email": application.company_contact_person_email, + "AddressStreet": application.company.street_address, + "AddressPostalCode": application.company.postcode, + "AddressCity": application.company.city, + } + ], + } + + def split_lines_at_semicolon(csv_string): # split CSV into lines and columns without using the csv library csv_lines = csv_string.splitlines() @@ -279,3 +353,8 @@ def pytest_sessionfinish(session, exitstatus): except OSError as e: print(f"Error while deleting file in media folder: {e}") print(f"\nTests finished, deleted {number_of_files} files in the media folder") + + +@pytest.fixture() +def ahjo_open_case_payload(decided_application): + return prepare_open_case_payload(decided_application) diff --git a/backend/benefit/applications/tests/factories.py b/backend/benefit/applications/tests/factories.py index e4a1e97255..116d32c909 100755 --- a/backend/benefit/applications/tests/factories.py +++ b/backend/benefit/applications/tests/factories.py @@ -89,7 +89,7 @@ class ApplicationFactory(factory.django.DjangoModelFactory): alternative_company_postcode = factory.Faker("postcode", locale="fi_FI") company_bank_account_number = factory.Faker("iban", locale="fi_FI") company_contact_person_phone_number = factory.Sequence( - lambda n: f"050-10000{n}" + lambda n: f"045191{n}" ) # max.length in validation seems to be 10 digits company_contact_person_email = factory.Faker("email", locale="fi_FI") company_contact_person_first_name = factory.Faker("first_name", locale="fi_FI") diff --git a/backend/benefit/applications/tests/test_ahjo_authentication.py b/backend/benefit/applications/tests/test_ahjo_authentication.py index 08a5d89951..89ba30d927 100644 --- a/backend/benefit/applications/tests/test_ahjo_authentication.py +++ b/backend/benefit/applications/tests/test_ahjo_authentication.py @@ -1,29 +1,32 @@ from datetime import datetime, timedelta -from unittest.mock import Mock import pytest -import requests +import requests_mock +from django.core.exceptions import ImproperlyConfigured from applications.models import AhjoSetting from applications.services.ahjo_authentication import AhjoConnector, AhjoToken @pytest.fixture -def requests_mock(): - return Mock(spec=requests.Session) +def ahjo_connector(): + return AhjoConnector() @pytest.fixture -def ahjo_connector(requests_mock: Mock): - return AhjoConnector(requests_mock) +def token_response(): + return { + "access_token": "access_token", + "refresh_token": "refresh_token", + "expires_in": "3600", + } -def test_is_configured(ahjo_connector: AhjoConnector): - # Test with all config options set - ahjo_connector.token_url = "https://example.com/token" - ahjo_connector.client_id = "client_id" - ahjo_connector.client_secret = "client_secret" - ahjo_connector.redirect_uri = "https://example.com/callback" +def test_is_configured(ahjo_connector): + ahjo_connector.AHJO_TOKEN_URL = "http://example.com/token" + ahjo_connector.AHJO_CLIENT_ID = "client_id" + ahjo_connector.AHJO_CLIENT_SECRET = "client_secret" + ahjo_connector.AHJO_REDIRECT_URL = "http://example.com/redirect" assert ahjo_connector.is_configured() is True # Test with missing config options @@ -31,45 +34,44 @@ def test_is_configured(ahjo_connector: AhjoConnector): assert ahjo_connector.is_configured() is False -def test_get_new_token(requests_mock, ahjo_connector: AhjoConnector): +def test_get_new_token(ahjo_connector, token_response): # Test with valid auth code - requests_mock.post.return_value.status_code = 200 - requests_mock.post.return_value.json.return_value = { - "access_token": "access_token", - "refresh_token": "refresh_token", - "expires_in": "3600", - } - token = ahjo_connector.get_new_token("auth_code") - assert token.access_token == "access_token" - assert token.refresh_token == "refresh_token" - assert isinstance(token.expires_in, datetime) + with requests_mock.Mocker() as m: + m.post( + "https://johdontyopoytahyte.hel.fi/ids4/connect/token", + json=token_response, + ) + + token = ahjo_connector.get_new_token("authcode123") + assert isinstance(token.expires_in, datetime) + assert token.access_token == "access_token" + assert token.refresh_token == "refresh_token" # Test with missing auth code - with pytest.raises(Exception): + with pytest.raises(ImproperlyConfigured): ahjo_connector.get_new_token("") -def test_refresh_token(requests_mock, ahjo_connector: AhjoConnector): +def test_refresh_token(ahjo_connector, token_response): # Test with valid refresh token AhjoSetting.objects.create( name="ahjo_access_token", data={ - "access_token": "dummy token", + "access_token": "access_token", "refresh_token": "refresh_token", "expires_in": datetime.now().isoformat(), }, ) + with requests_mock.Mocker() as m: + m.post( + "https://johdontyopoytahyte.hel.fi/ids4/connect/token", + json=token_response, + ) - requests_mock.post.return_value.status_code = 200 - requests_mock.post.return_value.json.return_value = { - "access_token": "new_access_token", - "refresh_token": "new_refresh_token", - "expires_in": "3600", - } - token = ahjo_connector.refresh_token() - assert token.access_token == "new_access_token" - assert token.refresh_token == "new_refresh_token" - assert isinstance(token.expires_in, datetime) + token = ahjo_connector.refresh_token() + assert token.access_token == "access_token" + assert token.refresh_token == "refresh_token" + assert isinstance(token.expires_in, datetime) # Test with missing refresh token AhjoSetting.objects.all().delete() @@ -77,24 +79,23 @@ def test_refresh_token(requests_mock, ahjo_connector: AhjoConnector): ahjo_connector.refresh_token() -def test_do_token_request(requests_mock, ahjo_connector: AhjoConnector): +def test_do_token_request(ahjo_connector: AhjoConnector, token_response): # Test with successful request - requests_mock.post.return_value.status_code = 200 - requests_mock.post.return_value.json.return_value = { - "access_token": "access_token", - "refresh_token": "refresh_token", - "expires_in": "3600", - } - payload = {"grant_type": "authorization_code", "code": "auth_code"} - token = ahjo_connector.do_token_request(payload) - assert token.access_token == "access_token" - assert token.refresh_token == "refresh_token" - assert isinstance(token.expires_in, datetime) - # Test with failed request - requests_mock.post.return_value.status_code = 400 - with pytest.raises(Exception): - ahjo_connector.do_token_request(payload) + with requests_mock.Mocker() as m: + m.post( + "https://johdontyopoytahyte.hel.fi/ids4/connect/token", json=token_response + ) + + token = ahjo_connector.do_token_request({}) + assert token.access_token == "access_token" + assert token.refresh_token == "refresh_token" + assert isinstance(token.expires_in, datetime) + + # Test with failed request + m.post("https://johdontyopoytahyte.hel.fi/ids4/connect/token", status_code=400) + with pytest.raises(Exception): + ahjo_connector.do_token_request({}) def test_get_token_from_db(ahjo_connector: AhjoConnector): diff --git a/backend/benefit/applications/tests/test_ahjo_integration.py b/backend/benefit/applications/tests/test_ahjo_integration.py index 7f67fb6da5..2ad185c4eb 100644 --- a/backend/benefit/applications/tests/test_ahjo_integration.py +++ b/backend/benefit/applications/tests/test_ahjo_integration.py @@ -6,6 +6,7 @@ from unittest.mock import patch import pytest +from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist from django.http import FileResponse from django.urls import reverse @@ -24,6 +25,7 @@ generate_composed_files, generate_single_approved_file, generate_single_declined_file, + get_application_for_ahjo, REJECTED_TITLE, ) from applications.tests.factories import ApplicationFactory, DecidedApplicationFactory @@ -33,6 +35,7 @@ from helsinkibenefit.tests.conftest import * # noqa from shared.common.tests.utils import normalize_whitespace from shared.service_bus.enums import YtjOrganizationCode +from users.models import User DE_MINIMIS_AID_PARTIAL_TEXT = ( # In English ~= "support is granted as insignificant i.e. de minimis support" @@ -409,3 +412,24 @@ def test_ahjo_callback_unauthorized_ip_not_allowed( response = ahjo_client.post(url, **auth_headers) assert response.status_code == 403 + + +@pytest.mark.django_db +def test_get_application_for_ahjo_success(decided_application): + user = decided_application.calculation.handler + User.objects.filter(pk=user.id).update(ad_username="foobar") + assert get_application_for_ahjo(decided_application.id) == decided_application + + +@pytest.mark.django_db +def test_get_application_for_ahjo_no_application(): + # Try to get an application with a non-existing id + with pytest.raises(ObjectDoesNotExist): + get_application_for_ahjo(uuid.uuid4()) + + +@pytest.mark.django_db +def test_get_application_for_ahjo_no_ad_username(decided_application): + # Try to get an application with a handler that has no ad_username + with pytest.raises(ImproperlyConfigured): + get_application_for_ahjo(decided_application.id) diff --git a/backend/benefit/applications/tests/test_ahjo_payload.py b/backend/benefit/applications/tests/test_ahjo_payload.py new file mode 100644 index 0000000000..b3fe4e9efa --- /dev/null +++ b/backend/benefit/applications/tests/test_ahjo_payload.py @@ -0,0 +1,95 @@ +from django.urls import reverse + +from applications.services.ahjo_payload import ( + _prepare_case_records, + _prepare_record, + _prepare_record_document_dict, + _prepare_top_level_dict, +) +from common.utils import hash_file + + +def test_prepare_record(decided_application, ahjo_payload_record): + application = decided_application + + record = _prepare_record( + ahjo_payload_record["Title"], + ahjo_payload_record["Type"], + application.created_at.isoformat(), + application.application_number, + [], + application.calculation.handler, + ) + + assert ahjo_payload_record == record + + +def test_prepare_record_document_dict(decided_application, settings): + settings.DEBUG = True + settings.API_BASE_URL = "http://test.com" + attachment = decided_application.attachments.first() + hash_value = hash_file(attachment.attachment_file) + file_url = reverse("ahjo_attachment_url", kwargs={"uuid": attachment.id}) + + want = { + "FileName": attachment.attachment_file.name, + "FormatName": attachment.content_type, + "HashAlgorithm": "sha256", + "HashValue": hash_value, + "FileURI": f"{settings.API_BASE_URL}{file_url}", + } + + got = _prepare_record_document_dict(attachment) + + assert want == got + + +def test_prepare_case_records(decided_application, settings): + settings.DEBUG = True + application = decided_application + handler = application.calculation.handler + handler_name = f"{handler.last_name}, {handler.first_name}" + want = [ + { + "Title": "Hakemus", + "Type": "hakemus", + "Acquired": application.created_at.isoformat(), + "PublicityClass": "Salassa pidettävä", + "SecurityReasons": ["JulkL (621/1999) 24.1 § 25 k"], + "Language": "fi", + "PersonalData": "Sisältää erityisiä henkilötietoja", + "Reference": str(application.application_number), + "Documents": [], + "Agents": [ + { + "Role": "mainCreator", + "Name": handler_name, + "ID": handler.ad_username, + } + ], + } + ] + + for attachment in application.attachments.all(): + document_record = _prepare_record( + "Hakemuksen Liite", + "liite", + attachment.created_at.isoformat(), + attachment.id, + [_prepare_record_document_dict(attachment)], + handler, + ) + + want.append(document_record) + + got = _prepare_case_records(application) + + assert want == got + + +def test_prepare_top_level_dict(decided_application, ahjo_open_case_top_level_dict): + application = decided_application + + got = _prepare_top_level_dict(application, []) + + assert ahjo_open_case_top_level_dict == got diff --git a/backend/benefit/applications/tests/test_ahjo_requests.py b/backend/benefit/applications/tests/test_ahjo_requests.py new file mode 100644 index 0000000000..4fea054f87 --- /dev/null +++ b/backend/benefit/applications/tests/test_ahjo_requests.py @@ -0,0 +1,89 @@ +import uuid +from unittest.mock import patch + +import requests +import requests_mock +from django.urls import reverse + +from applications.enums import AhjoStatus +from applications.services.ahjo_integration import ( + do_ahjo_request_with_json_payload, + prepare_headers, +) + + +def test_prepare_headers(settings): + settings.API_BASE_URL = "http://test.com" + access_token = "test_token" + application_uuid = uuid.uuid4() + + headers = prepare_headers(access_token, application_uuid) + + url = reverse("ahjo_callback_url", kwargs={"uuid": str(application_uuid)}) + expected_headers = { + "Authorization": f"Bearer {access_token}", + "Accept": "application/hal+json", + "X-CallbackURL": f"{settings.API_BASE_URL}{url}", + } + + assert headers == expected_headers + + +def test_do_ahjo_request_with_json_payload_success( + decided_application, ahjo_open_case_payload +): + url = "http://test.com" + headers = {"Authorization": "Bearer test"} + + with requests_mock.Mocker() as m: + m.post(f"{url}/cases", text="data") + do_ahjo_request_with_json_payload( + url, headers, ahjo_open_case_payload, decided_application + ) + decided_application.refresh_from_db() + assert ( + decided_application.ahjo_status.latest().status + == AhjoStatus.REQUEST_TO_OPEN_CASE_SENT + ) + + +@patch("applications.services.ahjo_integration.LOGGER") +def test_http_error(mock_logger, decided_application, ahjo_open_case_payload): + url = "http://mockedurl.com" + headers = {} + data = ahjo_open_case_payload + application = decided_application + + with requests_mock.Mocker() as m: + m.post(f"{url}/cases", status_code=400) + do_ahjo_request_with_json_payload(url, headers, data, application) + + mock_logger.error.assert_called() + + +@patch("applications.services.ahjo_integration.LOGGER") +def test_network_error(mock_logger, decided_application, ahjo_open_case_payload): + url = "http://mockedurl.com" + headers = {} + data = ahjo_open_case_payload + application = decided_application + + with requests_mock.Mocker() as m: + m.post(f"{url}/cases", exc=requests.exceptions.ConnectionError) + do_ahjo_request_with_json_payload(url, headers, data, application) + + mock_logger.error.assert_called() + + +@patch("applications.services.ahjo_integration.LOGGER") +def test_other_exception(mock_logger, decided_application, ahjo_open_case_payload): + url = "http://mockedurl.com" + headers = {} + data = ahjo_open_case_payload + application = decided_application + + with requests_mock.Mocker() as m: + m.post(f"{url}/cases", exc=Exception("Some error")) + do_ahjo_request_with_json_payload(url, headers, data, application) + + mock_logger.error.assert_called() diff --git a/backend/benefit/helsinkibenefit/settings.py b/backend/benefit/helsinkibenefit/settings.py index 2acddb29cc..2ed11f658b 100644 --- a/backend/benefit/helsinkibenefit/settings.py +++ b/backend/benefit/helsinkibenefit/settings.py @@ -20,6 +20,7 @@ default_var_root = environ.Path(checkout_dir("var")) env = environ.Env( + API_BASE_URL=(str, "https://localhost:8000"), DEBUG=(bool, False), SECRET_KEY=(str, ""), MEDIA_ROOT=(environ.Path(), default_var_root("media")), @@ -158,9 +159,9 @@ GDPR_API_QUERY_SCOPE=(str, "helsinkibenefit.gdprquery"), GDPR_API_DELETE_SCOPE=(str, "helsinkibenefit.gdprdelete"), # For AHJO Rest API authentication - AHJO_CLIENT_ID=(str, ""), - AHJO_CLIENT_SECRET=(str, ""), - AHJO_TOKEN_URL=(str, ""), + AHJO_CLIENT_ID=(str, "foo"), + AHJO_CLIENT_SECRET=(str, "bar"), + AHJO_TOKEN_URL=(str, "https://johdontyopoytahyte.hel.fi/ids4/connect/token"), AHJO_REST_API_URL=(str, "https://ahjohyte.hel.fi:9802/ahjorest/v1"), AHJO_REDIRECT_URL=(str, "https://helsinkilisa/dummyredirect.html"), AHJO_ALLOWED_IP=(str, ""), @@ -170,6 +171,8 @@ os.environ["HTTPS"] = "on" +API_BASE_URL = env.str("API_BASE_URL") + BASE_DIR = str(checkout_dir) DEBUG = env.bool("DEBUG")