From 8f352e880aaa045b44d09cf2d694ca225c5dc4dd Mon Sep 17 00:00:00 2001 From: Prafful Sharma <115104695+DraKen0009@users.noreply.github.com> Date: Thu, 14 Nov 2024 19:45:02 +0530 Subject: [PATCH 1/9] added test cases for asset bed (#2532) added test cases for asset bed (#2532) --- care/facility/api/serializers/asset.py | 9 +- care/facility/api/serializers/bed.py | 12 +- care/facility/tests/test_asset_bed_api.py | 219 -------- .../facility/tests/test_asset_location_api.py | 6 +- care/facility/tests/test_assetbed_api.py | 531 ++++++++++++++++++ care/facility/tests/test_bed_api.py | 17 +- care/utils/tests/test_utils.py | 12 +- 7 files changed, 571 insertions(+), 235 deletions(-) delete mode 100644 care/facility/tests/test_asset_bed_api.py create mode 100644 care/facility/tests/test_assetbed_api.py diff --git a/care/facility/api/serializers/asset.py b/care/facility/api/serializers/asset.py index f403361e1a..ddb6a8e5d4 100644 --- a/care/facility/api/serializers/asset.py +++ b/care/facility/api/serializers/asset.py @@ -7,7 +7,7 @@ from django.utils.timezone import now from drf_spectacular.utils import extend_schema_field from rest_framework import serializers -from rest_framework.exceptions import ValidationError +from rest_framework.exceptions import PermissionDenied, ValidationError from rest_framework.serializers import ( CharField, JSONField, @@ -174,11 +174,14 @@ def validate(self, attrs): facilities = get_facility_queryset(user) if not facilities.filter(id=location.facility.id).exists(): - raise PermissionError + error_message = ( + "You do not have permission to access this facility's asset." + ) + raise PermissionDenied(error_message) del attrs["location"] attrs["current_location"] = location - # validate that warraty date is not in the past + # validate that warranty date is not in the past if warranty_amc_end_of_validity := attrs.get("warranty_amc_end_of_validity"): # pop out warranty date if it is not changed if ( diff --git a/care/facility/api/serializers/bed.py b/care/facility/api/serializers/bed.py index 031d2a68c1..508bc25bc4 100644 --- a/care/facility/api/serializers/bed.py +++ b/care/facility/api/serializers/bed.py @@ -2,7 +2,7 @@ from django.db.models import Exists, OuterRef, Q from django.shortcuts import get_object_or_404 from django.utils import timezone -from rest_framework.exceptions import ValidationError +from rest_framework.exceptions import PermissionDenied, ValidationError from rest_framework.serializers import ( BooleanField, CharField, @@ -74,7 +74,10 @@ def validate(self, attrs): if (not facilities.filter(id=location.facility.id).exists()) or ( not facilities.filter(id=facility.id).exists() ): - raise PermissionError + error_message = ( + "You do not have permission to access this facility's bed." + ) + raise PermissionDenied(error_message) del attrs["location"] attrs["location"] = location attrs["facility"] = facility @@ -110,7 +113,10 @@ def validate(self, attrs): if ( not facilities.filter(id=asset.current_location.facility.id).exists() ) or (not facilities.filter(id=bed.facility.id).exists()): - raise PermissionError + error_message = ( + "You do not have permission to access this facility's assetbed." + ) + raise PermissionDenied(error_message) if AssetBed.objects.filter(asset=asset, bed=bed).exists(): raise ValidationError( {"non_field_errors": "Asset is already linked to bed"} diff --git a/care/facility/tests/test_asset_bed_api.py b/care/facility/tests/test_asset_bed_api.py deleted file mode 100644 index 4ed81a36b8..0000000000 --- a/care/facility/tests/test_asset_bed_api.py +++ /dev/null @@ -1,219 +0,0 @@ -from rest_framework import status -from rest_framework.test import APITestCase - -from care.users.models import User -from care.utils.assetintegration.asset_classes import AssetClasses -from care.utils.tests.test_utils import TestUtils - - -class AssetBedViewSetTestCase(TestUtils, APITestCase): - @classmethod - def setUpTestData(cls): - cls.state = cls.create_state() - cls.district = cls.create_district(cls.state) - cls.local_body = cls.create_local_body(cls.district) - cls.super_user = cls.create_super_user("su", cls.district) - cls.facility = cls.create_facility(cls.super_user, cls.district, cls.local_body) - cls.user = cls.create_user( - User.TYPE_VALUE_MAP["DistrictAdmin"], - cls.district, - home_facility=cls.facility, - ) - cls.asset_location = cls.create_asset_location(cls.facility) - cls.asset = cls.create_asset(cls.asset_location) - cls.monitor_asset_1 = cls.create_asset( - cls.asset_location, asset_class=AssetClasses.HL7MONITOR.name - ) - cls.monitor_asset_2 = cls.create_asset( - cls.asset_location, asset_class=AssetClasses.HL7MONITOR.name - ) - cls.camera_asset = cls.create_asset( - cls.asset_location, asset_class=AssetClasses.ONVIF.name - ) - cls.camera_asset_1 = cls.create_asset( - cls.asset_location, asset_class=AssetClasses.ONVIF.name, name="Camera 1" - ) - cls.camera_asset_2 = cls.create_asset( - cls.asset_location, asset_class=AssetClasses.ONVIF.name, name="Camera 2" - ) - cls.bed = cls.create_bed(cls.facility, cls.asset_location) - - def test_link_disallowed_asset_class_asset_to_bed(self): - data = { - "asset": self.asset.external_id, - "bed": self.bed.external_id, - } - res = self.client.post("/api/v1/assetbed/", data) - self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) - - def test_link_asset_to_bed_and_attempt_duplicate_linking(self): - data = { - "asset": self.camera_asset.external_id, - "bed": self.bed.external_id, - } - res = self.client.post("/api/v1/assetbed/", data) - self.assertEqual(res.status_code, status.HTTP_201_CREATED) - # Attempt linking same camera to the same bed again. - res = self.client.post("/api/v1/assetbed/", data) - self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) - # List asset beds filtered by asset and bed ID and check only 1 result exists - res = self.client.get("/api/v1/assetbed/", data) - self.assertEqual(res.status_code, status.HTTP_200_OK) - self.assertEqual(res.data["count"], 1) - - def test_linking_multiple_cameras_to_a_bed(self): - data = { - "asset": self.camera_asset_1.external_id, - "bed": self.bed.external_id, - } - res = self.client.post("/api/v1/assetbed/", data) - self.assertEqual(res.status_code, status.HTTP_201_CREATED) - # Attempt linking another camera to same bed. - data["asset"] = self.camera_asset_2.external_id - res = self.client.post("/api/v1/assetbed/", data) - self.assertEqual(res.status_code, status.HTTP_201_CREATED) - - def test_linking_multiple_hl7_monitors_to_a_bed(self): - data = { - "asset": self.monitor_asset_1.external_id, - "bed": self.bed.external_id, - } - res = self.client.post("/api/v1/assetbed/", data) - self.assertEqual(res.status_code, status.HTTP_201_CREATED) - # Attempt linking another hl7 monitor to same bed. - data["asset"] = self.monitor_asset_2.external_id - res = self.client.post("/api/v1/assetbed/", data) - self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) - - -class AssetBedCameraPresetViewSetTestCase(TestUtils, APITestCase): - @classmethod - def setUpTestData(cls): - cls.state = cls.create_state() - cls.district = cls.create_district(cls.state) - cls.local_body = cls.create_local_body(cls.district) - cls.super_user = cls.create_super_user("su", cls.district) - cls.facility = cls.create_facility(cls.super_user, cls.district, cls.local_body) - cls.user = cls.create_user( - User.TYPE_VALUE_MAP["DistrictAdmin"], - cls.district, - home_facility=cls.facility, - ) - cls.asset_location = cls.create_asset_location(cls.facility) - cls.asset1 = cls.create_asset( - cls.asset_location, asset_class=AssetClasses.ONVIF.name - ) - cls.asset2 = cls.create_asset( - cls.asset_location, asset_class=AssetClasses.ONVIF.name - ) - cls.bed = cls.create_bed(cls.facility, cls.asset_location) - cls.asset_bed1 = cls.create_asset_bed(cls.asset1, cls.bed) - cls.asset_bed2 = cls.create_asset_bed(cls.asset2, cls.bed) - - def get_base_url(self, asset_bed_id=None): - return f"/api/v1/assetbed/{asset_bed_id or self.asset_bed1.external_id}/camera_presets/" - - def test_create_camera_preset_without_position(self): - res = self.client.post( - self.get_base_url(), - { - "name": "Preset without position", - "position": {}, - }, - format="json", - ) - self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) - - def test_create_camera_preset_with_missing_required_keys_in_position(self): - res = self.client.post( - self.get_base_url(), - { - "name": "Preset with invalid position", - "position": {"key": "value"}, - }, - format="json", - ) - self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) - - def test_create_camera_preset_with_position_not_number(self): - res = self.client.post( - self.get_base_url(), - { - "name": "Preset with invalid position", - "position": { - "x": "not a number", - "y": 1, - "zoom": 1, - }, - }, - format="json", - ) - self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) - - def test_create_camera_preset_with_position_values_as_string(self): - res = self.client.post( - self.get_base_url(), - { - "name": "Preset with invalid position", - "position": { - "x": "1", - "y": "1", - "zoom": "1", - }, - }, - format="json", - ) - self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) - - def test_create_camera_preset_and_presence_in_various_preset_list_apis(self): - asset_bed = self.asset_bed1 - res = self.client.post( - self.get_base_url(asset_bed.external_id), - { - "name": "Preset with proper position", - "position": { - "x": 1.0, - "y": 1.0, - "zoom": 1.0, - }, - }, - format="json", - ) - self.assertEqual(res.status_code, status.HTTP_201_CREATED) - preset_external_id = res.data["id"] - - # Check if preset in asset-bed preset list - res = self.client.get(self.get_base_url(asset_bed.external_id)) - self.assertEqual(res.status_code, status.HTTP_200_OK) - self.assertContains(res, preset_external_id) - - # Check if preset in asset preset list - res = self.client.get( - f"/api/v1/asset/{asset_bed.asset.external_id}/camera_presets/" - ) - self.assertEqual(res.status_code, status.HTTP_200_OK) - self.assertContains(res, preset_external_id) - - # Check if preset in bed preset list - res = self.client.get( - f"/api/v1/bed/{asset_bed.bed.external_id}/camera_presets/" - ) - self.assertEqual(res.status_code, status.HTTP_200_OK) - self.assertContains(res, preset_external_id) - - def test_create_camera_preset_with_same_name_in_same_bed(self): - data = { - "name": "Duplicate Preset Name", - "position": { - "x": 1.0, - "y": 1.0, - "zoom": 1.0, - }, - } - self.client.post( - self.get_base_url(self.asset_bed1.external_id), data, format="json" - ) - res = self.client.post( - self.get_base_url(self.asset_bed2.external_id), data, format="json" - ) - self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) diff --git a/care/facility/tests/test_asset_location_api.py b/care/facility/tests/test_asset_location_api.py index 9e8280d617..6bcfed7850 100644 --- a/care/facility/tests/test_asset_location_api.py +++ b/care/facility/tests/test_asset_location_api.py @@ -21,7 +21,7 @@ def setUpTestData(cls) -> None: asset_class=AssetClasses.HL7MONITOR.name, ) cls.bed = cls.create_bed(cls.facility, cls.asset_location_with_linked_bed) - cls.asset_bed = cls.create_asset_bed(cls.asset, cls.bed) + cls.asset_bed = cls.create_assetbed(cls.bed, cls.asset) cls.patient = cls.create_patient(cls.district, cls.facility) cls.consultation = cls.create_consultation(cls.patient, cls.facility) cls.consultation_bed = cls.create_consultation_bed(cls.consultation, cls.bed) @@ -36,8 +36,8 @@ def setUpTestData(cls) -> None: cls.asset_second_location, asset_class=AssetClasses.HL7MONITOR.name ) cls.asset_bed_second = cls.create_bed(cls.facility, cls.asset_second_location) - cls.assetbed_second = cls.create_asset_bed( - cls.asset_second, cls.asset_bed_second + cls.assetbed_second = cls.create_assetbed( + cls.asset_bed_second, cls.asset_second ) def test_list_asset_locations(self): diff --git a/care/facility/tests/test_assetbed_api.py b/care/facility/tests/test_assetbed_api.py new file mode 100644 index 0000000000..2129ef1a00 --- /dev/null +++ b/care/facility/tests/test_assetbed_api.py @@ -0,0 +1,531 @@ +from rest_framework import status +from rest_framework.test import APITestCase + +from care.facility.models import AssetBed, Bed +from care.users.models import User +from care.utils.assetintegration.asset_classes import AssetClasses +from care.utils.tests.test_utils import TestUtils + + +class AssetBedViewSetTests(TestUtils, APITestCase): + """ + Test class for AssetBed + """ + + @classmethod + def setUpTestData(cls) -> None: + cls.state = cls.create_state() + cls.district = cls.create_district(cls.state) + cls.local_body = cls.create_local_body(cls.district) + cls.super_user = cls.create_super_user("su", cls.district) + cls.facility = cls.create_facility(cls.super_user, cls.district, cls.local_body) + cls.facility2 = cls.create_facility( + cls.super_user, cls.district, cls.local_body + ) + cls.user = cls.create_user( + "user", + district=cls.district, + local_body=cls.local_body, + home_facility=cls.facility, + ) # user from facility + cls.foreign_user = cls.create_user( + "foreign_user", + district=cls.district, + local_body=cls.local_body, + home_facility=cls.facility2, + ) # user outside the facility + cls.patient = cls.create_patient( + cls.district, cls.facility, local_body=cls.local_body + ) + cls.asset_location1 = cls.create_asset_location(cls.facility) + cls.asset1 = cls.create_asset( + cls.asset_location1, asset_class=AssetClasses.HL7MONITOR.name + ) + cls.bed1 = Bed.objects.create( + name="bed1", location=cls.asset_location1, facility=cls.facility + ) + cls.asset_location2 = cls.create_asset_location(cls.facility) + # camera asset + cls.asset2 = cls.create_asset( + cls.asset_location2, asset_class=AssetClasses.ONVIF.name + ) + cls.bed2 = Bed.objects.create( + name="bed2", location=cls.asset_location2, facility=cls.facility + ) + cls.asset_location3 = cls.create_asset_location(cls.facility) + cls.asset3 = cls.create_asset( + cls.asset_location3, asset_class=AssetClasses.VENTILATOR.name + ) + cls.bed3 = Bed.objects.create( + name="bed3", location=cls.asset_location3, facility=cls.facility + ) + # for testing create, put and patch requests + cls.bed4 = Bed.objects.create( + name="bed4", location=cls.asset_location3, facility=cls.facility + ) + cls.foreign_asset_location = cls.create_asset_location(cls.facility2) + cls.foreign_asset = cls.create_asset(cls.foreign_asset_location) + cls.foreign_bed = Bed.objects.create( + name="foreign_bed", + location=cls.foreign_asset_location, + facility=cls.facility2, + ) + + cls.create_assetbed(bed=cls.bed2, asset=cls.asset2) + cls.create_assetbed(bed=cls.bed3, asset=cls.asset3) + + # assetbed for different facility + cls.create_assetbed(bed=cls.foreign_bed, asset=cls.foreign_asset) + + def setUp(self) -> None: + super().setUp() + self.assetbed = self.create_assetbed(bed=self.bed1, asset=self.asset1) + + def get_base_url(self) -> str: + return "/api/v1/assetbed" + + def get_url(self, external_id=None): + """ + Constructs the url for ambulance api + """ + base_url = f"{self.get_base_url()}/" + if external_id is not None: + base_url += f"{external_id}/" + return base_url + + def test_list_assetbed(self): + # assetbed accessible to facility 1 user (current user) + response = self.client.get(self.get_url()) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data["count"], 3) + + # logging in as foreign user + self.client.force_login(self.foreign_user) + + # assetbed accessible to facility 2 user (foreign user) + response = self.client.get(self.get_url()) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data["count"], 1) + + # logging in as superuser + self.client.force_login(self.super_user) + + # access to all assetbed + response = self.client.get(self.get_url()) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data["count"], AssetBed.objects.count()) + + # testing for filters + response = self.client.get(self.get_url(), {"asset": self.asset1.external_id}) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual( + response.data["count"], AssetBed.objects.filter(asset=self.asset1).count() + ) + + response = self.client.get(self.get_url(), {"bed": self.bed1.external_id}) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual( + response.data["count"], AssetBed.objects.filter(bed=self.bed1).count() + ) + self.assertEqual( + response.data["results"][0]["bed_object"]["name"], self.bed1.name + ) + + response = self.client.get( + self.get_url(), {"facility": self.facility.external_id} + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual( + response.data["count"], + AssetBed.objects.filter(bed__facility=self.facility).count(), + ) + + def test_create_assetbed(self): + # Missing asset and bed + missing_fields_data = {} + response = self.client.post(self.get_url(), missing_fields_data, format="json") + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertIn("asset", response.data) + self.assertIn("bed", response.data) + + # Invalid asset UUID + invalid_asset_uuid_data = { + "asset": "invalid-uuid", + "bed": str(self.bed1.external_id), + } + response = self.client.post( + self.get_url(), invalid_asset_uuid_data, format="json" + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertIn("asset", response.data) + + # Invalid bed UUID + invalid_bed_uuid_data = { + "asset": str(self.asset1.external_id), + "bed": "invalid-uuid", + } + response = self.client.post( + self.get_url(), invalid_bed_uuid_data, format="json" + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertIn("bed", response.data) + + # Non-existent asset UUID + non_existent_asset_uuid_data = { + "asset": "11111111-1111-1111-1111-111111111111", + "bed": str(self.bed1.external_id), + } + response = self.client.post( + self.get_url(), non_existent_asset_uuid_data, format="json" + ) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + # Non-existent bed UUID + non_existent_bed_uuid_data = { + "asset": str(self.asset1.external_id), + "bed": "11111111-1111-1111-1111-111111111111", + } + response = self.client.post( + self.get_url(), non_existent_bed_uuid_data, format="json" + ) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + # User does not have access to foreign facility + foreign_user_data = { + "asset": str(self.foreign_asset.external_id), + "bed": str(self.foreign_bed.external_id), + } + self.client.force_login(self.user) # Ensure current user is logged in + response = self.client.post(self.get_url(), foreign_user_data, format="json") + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + # Invalid asset class (e.g., VENTILATOR) + invalid_asset_class_data = { + "asset": str(self.asset3.external_id), # VENTILATOR asset class + "bed": str(self.bed1.external_id), + } + response = self.client.post( + self.get_url(), invalid_asset_class_data, format="json" + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertIn("asset", response.data) + + # Asset and bed in different facilities + asset_bed_different_facilities = { + "asset": str(self.foreign_asset.external_id), + "bed": str(self.bed1.external_id), + } + response = self.client.post( + self.get_url(), asset_bed_different_facilities, format="json" + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + # Trying to create a duplicate assetbed with bed2 and asset2 (assetbed already exist with same bed and asset) + duplicate_asset_class_data = { + "asset": str(self.asset2.external_id), # asset2 is already assigned to bed2 + "bed": str(self.bed2.external_id), + } + response = self.client.post( + self.get_url(), duplicate_asset_class_data, format="json" + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + # Successfully create AssetBed with valid data + valid_data = { + "asset": str(self.asset1.external_id), + "bed": str(self.bed4.external_id), + } + response = self.client.post(self.get_url(), valid_data, format="json") + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + def test_retrieve_assetbed(self): + response = self.client.get(self.get_url(external_id=self.assetbed.external_id)) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual( + response.data["asset_object"]["id"], str(self.assetbed.asset.external_id) + ) + self.assertEqual( + response.data["bed_object"]["id"], str(self.assetbed.bed.external_id) + ) + + def test_update_assetbed(self): + # checking old values + response = self.client.get(self.get_url(external_id=self.assetbed.external_id)) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data["meta"], {}) + self.assertEqual( + response.data["asset_object"]["id"], str(self.assetbed.asset.external_id) + ) + self.assertEqual( + response.data["bed_object"]["id"], str(self.assetbed.bed.external_id) + ) + + invalid_updated_data = { + "asset": self.asset2.external_id, + "meta": {"sample_data": "sample value"}, + } + response = self.client.put( + self.get_url(external_id=self.assetbed.external_id), + invalid_updated_data, + format="json", + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + invalid_updated_data = { + "bed": self.bed2.external_id, + "meta": {"sample_data": "sample value"}, + } + response = self.client.put( + self.get_url(external_id=self.assetbed.external_id), + invalid_updated_data, + format="json", + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + updated_data = { + "bed": self.bed4.external_id, + "asset": self.asset2.external_id, + "meta": {"sample_data": "sample value"}, + } + response = self.client.put( + self.get_url(external_id=self.assetbed.external_id), + updated_data, + format="json", + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + self.assetbed.refresh_from_db() + + self.assertEqual(self.assetbed.bed.external_id, self.bed4.external_id) + self.assertEqual(self.assetbed.asset.external_id, self.asset2.external_id) + self.assertEqual(self.assetbed.meta, {"sample_data": "sample value"}) + + def test_patch_assetbed(self): + # checking old values + response = self.client.get(self.get_url(external_id=self.assetbed.external_id)) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data["meta"], {}) + self.assertEqual( + response.data["asset_object"]["id"], str(self.assetbed.asset.external_id) + ) + self.assertEqual( + response.data["bed_object"]["id"], str(self.assetbed.bed.external_id) + ) + + invalid_updated_data = { + "asset": self.asset2.external_id, + "meta": {"sample_data": "sample value"}, + } + response = self.client.patch( + self.get_url(external_id=self.assetbed.external_id), + invalid_updated_data, + format="json", + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + invalid_updated_data = { + "bed": self.bed4.external_id, + "meta": {"sample_data": "sample value"}, + } + response = self.client.patch( + self.get_url(external_id=self.assetbed.external_id), + invalid_updated_data, + format="json", + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + updated_data = { + "bed": self.bed4.external_id, + "asset": self.asset2.external_id, + } + + response = self.client.patch( + self.get_url(external_id=self.assetbed.external_id), + updated_data, + format="json", + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + self.assetbed.refresh_from_db() + + self.assertEqual(self.assetbed.bed.external_id, self.bed4.external_id) + self.assertEqual(self.assetbed.asset.external_id, self.asset2.external_id) + + def test_delete_assetbed(self): + # confirming that the object exist + response = self.client.get(self.get_url(external_id=self.assetbed.external_id)) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + response = self.client.delete( + self.get_url(external_id=self.assetbed.external_id) + ) + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + + # confirming if it's deleted + response = self.client.get(self.get_url(external_id=self.assetbed.external_id)) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + # confirming using db + self.assetbed.refresh_from_db() + self.assertFalse( + AssetBed.objects.filter(external_id=self.assetbed.external_id).exists() + ) + + def test_linking_multiple_cameras_to_a_bed(self): + # We already have camera linked(asset2) to bed2 + # Attempt linking another camera to same bed. + new_camera_asset = self.create_asset( + self.asset_location2, asset_class=AssetClasses.ONVIF.name + ) + data = { + "bed": self.bed2.external_id, + "asset": new_camera_asset.external_id, + } + res = self.client.post(self.get_url(), data, format="json") + self.assertEqual(res.status_code, status.HTTP_201_CREATED) + + def test_linking_multiple_hl7_monitors_to_a_bed(self): + # We already have hl7 monitor linked(asset1) to bed1) + # Attempt linking another hl7 monitor to same bed. + new_hl7_monitor_asset = self.create_asset( + self.asset_location2, asset_class=AssetClasses.HL7MONITOR.name + ) + data = { + "bed": self.bed1.external_id, + "asset": new_hl7_monitor_asset.external_id, + } + res = self.client.post("/api/v1/assetbed/", data, format="json") + self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) + + +class AssetBedCameraPresetViewSetTestCase(TestUtils, APITestCase): + @classmethod + def setUpTestData(cls): + cls.state = cls.create_state() + cls.district = cls.create_district(cls.state) + cls.local_body = cls.create_local_body(cls.district) + cls.super_user = cls.create_super_user("su", cls.district) + cls.facility = cls.create_facility(cls.super_user, cls.district, cls.local_body) + cls.user = cls.create_user( + User.TYPE_VALUE_MAP["DistrictAdmin"], + cls.district, + home_facility=cls.facility, + ) + cls.asset_location = cls.create_asset_location(cls.facility) + cls.asset1 = cls.create_asset( + cls.asset_location, asset_class=AssetClasses.ONVIF.name + ) + cls.asset2 = cls.create_asset( + cls.asset_location, asset_class=AssetClasses.ONVIF.name + ) + cls.bed = cls.create_bed(cls.facility, cls.asset_location) + cls.asset_bed1 = cls.create_assetbed(cls.bed, cls.asset1) + cls.asset_bed2 = cls.create_assetbed(cls.bed, cls.asset2) + + def get_base_url(self, asset_bed_id=None): + return f"/api/v1/assetbed/{asset_bed_id or self.asset_bed1.external_id}/camera_presets/" + + def test_create_camera_preset_without_position(self): + res = self.client.post( + self.get_base_url(), + { + "name": "Preset without position", + "position": {}, + }, + format="json", + ) + self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) + + def test_create_camera_preset_with_missing_required_keys_in_position(self): + res = self.client.post( + self.get_base_url(), + { + "name": "Preset with invalid position", + "position": {"key": "value"}, + }, + format="json", + ) + self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) + + def test_create_camera_preset_with_position_not_number(self): + res = self.client.post( + self.get_base_url(), + { + "name": "Preset with invalid position", + "position": { + "x": "not a number", + "y": 1, + "zoom": 1, + }, + }, + format="json", + ) + self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) + + def test_create_camera_preset_with_position_values_as_string(self): + res = self.client.post( + self.get_base_url(), + { + "name": "Preset with invalid position", + "position": { + "x": "1", + "y": "1", + "zoom": "1", + }, + }, + format="json", + ) + self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) + + def test_create_camera_preset_and_presence_in_various_preset_list_apis(self): + asset_bed = self.asset_bed1 + res = self.client.post( + self.get_base_url(asset_bed.external_id), + { + "name": "Preset with proper position", + "position": { + "x": 1.0, + "y": 1.0, + "zoom": 1.0, + }, + }, + format="json", + ) + self.assertEqual(res.status_code, status.HTTP_201_CREATED) + preset_external_id = res.data["id"] + + # Check if preset in asset-bed preset list + res = self.client.get(self.get_base_url(asset_bed.external_id)) + self.assertEqual(res.status_code, status.HTTP_200_OK) + self.assertContains(res, preset_external_id) + + # Check if preset in asset preset list + res = self.client.get( + f"/api/v1/asset/{asset_bed.asset.external_id}/camera_presets/" + ) + self.assertEqual(res.status_code, status.HTTP_200_OK) + self.assertContains(res, preset_external_id) + + # Check if preset in bed preset list + res = self.client.get( + f"/api/v1/bed/{asset_bed.bed.external_id}/camera_presets/" + ) + self.assertEqual(res.status_code, status.HTTP_200_OK) + self.assertContains(res, preset_external_id) + + def test_create_camera_preset_with_same_name_in_same_bed(self): + data = { + "name": "Duplicate Preset Name", + "position": { + "x": 1.0, + "y": 1.0, + "zoom": 1.0, + }, + } + self.client.post( + self.get_base_url(self.asset_bed1.external_id), data, format="json" + ) + res = self.client.post( + self.get_base_url(self.asset_bed2.external_id), data, format="json" + ) + self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) diff --git a/care/facility/tests/test_bed_api.py b/care/facility/tests/test_bed_api.py index ce334dd6e4..9bceece342 100644 --- a/care/facility/tests/test_bed_api.py +++ b/care/facility/tests/test_bed_api.py @@ -75,7 +75,7 @@ def test_list_beds(self): self.client.logout() def test_create_beds(self): - sample_data = { + base_data = { "name": "Sample Beds", "bed_type": 2, "location": self.asset_location.external_id, @@ -83,23 +83,27 @@ def test_create_beds(self): "number_of_beds": 10, "description": "This is a sample bed description", } + sample_data = base_data.copy() # Create a fresh copy of the base data response = self.client.post("/api/v1/bed/", sample_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertEqual(Bed.objects.filter(bed_type=2).count(), 10) # without location + sample_data = base_data.copy() sample_data.update({"location": None}) response = self.client.post("/api/v1/bed/", sample_data, format="json") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual(response.data["location"][0], "This field may not be null.") # without facility + sample_data = base_data.copy() sample_data.update({"facility": None}) response = self.client.post("/api/v1/bed/", sample_data, format="json") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual(response.data["facility"][0], "This field may not be null.") # Test - if beds > 100 + sample_data = base_data.copy() sample_data.update({"number_of_beds": 200}) response = self.client.post("/api/v1/bed/", sample_data, format="json") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) @@ -108,6 +112,17 @@ def test_create_beds(self): "Cannot create more than 100 beds at once.", ) + # creating bed in different facility + sample_data = base_data.copy() + sample_data.update( + { + "location": self.asset_location2.external_id, + "facility": self.facility2.external_id, + } + ) + response = self.client.post("/api/v1/bed/", sample_data, format="json") + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + def test_retrieve_bed(self): response = self.client.get(f"/api/v1/bed/{self.bed1.external_id}/") self.assertEqual(response.status_code, status.HTTP_200_OK) diff --git a/care/utils/tests/test_utils.py b/care/utils/tests/test_utils.py index 91d4ac8d67..fbc286a337 100644 --- a/care/utils/tests/test_utils.py +++ b/care/utils/tests/test_utils.py @@ -446,12 +446,6 @@ def create_bed(cls, facility: Facility, location: AssetLocation, **kwargs): data.update(kwargs) return Bed.objects.create(**data) - @classmethod - def create_asset_bed(cls, asset: Asset, bed: Bed, **kwargs): - data = {"asset": asset, "bed": bed} - data.update(kwargs) - return AssetBed.objects.create(**data) - @classmethod def create_consultation_bed( cls, @@ -728,6 +722,12 @@ def create_prescription( data.update(**kwargs) return Prescription.objects.create(**data) + @classmethod + def create_assetbed(cls, bed: Bed, asset: Asset, **kwargs) -> AssetBed: + data = {"bed": bed, "asset": asset} + data.update(kwargs) + return AssetBed.objects.create(**data) + def get_list_representation(self, obj) -> dict: """ Returns the dict representation of the obj in list API From 05cf1cf8a8ff894828170b47fdb8a6ed077d0e5e Mon Sep 17 00:00:00 2001 From: Prafful Sharma <115104695+DraKen0009@users.noreply.github.com> Date: Thu, 14 Nov 2024 19:45:16 +0530 Subject: [PATCH 2/9] added tests for patient transfer (#2564) added tests for patient transfer (#2564) --- care/facility/api/viewsets/patient.py | 2 +- care/facility/tests/test_patient_api.py | 164 +++++++++++++++++++++++- 2 files changed, 164 insertions(+), 2 deletions(-) diff --git a/care/facility/api/viewsets/patient.py b/care/facility/api/viewsets/patient.py index 72731cd6e2..963b6d4731 100644 --- a/care/facility/api/viewsets/patient.py +++ b/care/facility/api/viewsets/patient.py @@ -589,7 +589,7 @@ def list(self, request, *args, **kwargs): @action(detail=True, methods=["POST"]) def transfer(self, request, *args, **kwargs): patient = PatientRegistration.objects.get(external_id=kwargs["external_id"]) - facility = Facility.objects.get(external_id=request.data["facility"]) + facility = get_object_or_404(Facility, external_id=request.data["facility"]) if patient.is_expired: return Response( diff --git a/care/facility/tests/test_patient_api.py b/care/facility/tests/test_patient_api.py index 6facfdd3ad..37accc7c2c 100644 --- a/care/facility/tests/test_patient_api.py +++ b/care/facility/tests/test_patient_api.py @@ -4,7 +4,7 @@ from rest_framework import status from rest_framework.test import APITestCase -from care.facility.models import PatientNoteThreadChoices +from care.facility.models import PatientNoteThreadChoices, ShiftingRequest from care.facility.models.file_upload import FileUpload from care.facility.models.icd11_diagnosis import ( ConditionVerificationStatus, @@ -913,6 +913,8 @@ def setUpTestData(cls): cls.patient.save() def test_patient_transfer(self): + # test transfer of patient to a outside facility with allow_transfer set to "True" + # test transfer patient with dob self.client.force_authenticate(user=self.super_user) response = self.client.post( f"/api/v1/patient/{self.patient.external_id}/transfer/", @@ -1000,6 +1002,166 @@ def test_transfer_disallowed_by_facility(self): "Patient transfer cannot be completed because the source facility does not permit it", ) + def test_transfer_within_facility(self): + self.client.force_authenticate(user=self.super_user) + response = self.client.post( + f"/api/v1/patient/{self.patient.external_id}/transfer/", + { + "date_of_birth": "1992-04-01", + "facility": self.facility.external_id, + }, + ) + self.assertEqual(response.status_code, status.HTTP_406_NOT_ACCEPTABLE) + self.assertEqual( + response.data["Patient"], + "Patient transfer cannot be completed because the patient has an active consultation in the same facility", + ) + + def test_transfer_without_dob(self): + self.client.force_authenticate(user=self.super_user) + response = self.client.post( + f"/api/v1/patient/{self.patient.external_id}/transfer/", + { + "age": "32", + "facility": self.destination_facility.external_id, + }, + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + self.patient.refresh_from_db() + self.consultation.refresh_from_db() + + self.assertEqual(self.patient.facility, self.destination_facility) + + self.assertEqual( + self.consultation.new_discharge_reason, NewDischargeReasonEnum.REFERRED + ) + self.assertIsNotNone(self.consultation.discharge_date) + + def test_transfer_with_no_active_consultation(self): + # Mocking discharge of the patient + self.consultation.discharge_date = now() + self.consultation.save() + + # Ensure transfer succeeds when there's no active consultation + self.client.force_authenticate(user=self.super_user) + response = self.client.post( + f"/api/v1/patient/{self.patient.external_id}/transfer/", + { + "year_of_birth": 1992, + "facility": self.destination_facility.external_id, + }, + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + # Refresh patient data + self.patient.refresh_from_db() + + # Assert the patient's facility has been updated + self.assertEqual(self.patient.facility, self.destination_facility) + + def test_transfer_with_incorrect_year_of_birth(self): + # Set the patient's facility to allow transfers + self.patient.allow_transfer = True + self.patient.save() + + self.client.force_authenticate(user=self.super_user) + response = self.client.post( + f"/api/v1/patient/{self.patient.external_id}/transfer/", + { + "year_of_birth": 1990, # Incorrect year of birth + "facility": self.destination_facility.external_id, + }, + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual( + response.data["year_of_birth"][0], "Year of birth does not match" + ) + + def test_auto_reject_active_shifting_requests_upon_transfer(self): + # Create a mock shifting request that is still active (PENDING status) + shifting_request = ShiftingRequest.objects.create( + patient=self.patient, + origin_facility=self.facility, + status=10, # PENDING status + comments="Initial request", + created_by=self.super_user, + ) + + # Perform the patient transfer + self.client.force_authenticate(user=self.super_user) + response = self.client.post( + f"/api/v1/patient/{self.patient.external_id}/transfer/", + { + "year_of_birth": 1992, + "facility": self.destination_facility.external_id, + }, + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + # Refresh the shifting request and verify it was auto-rejected + shifting_request.refresh_from_db() + self.assertEqual(shifting_request.status, 30) # REJECTED status + self.assertIn( + f"The shifting request was auto rejected by the system as the patient was moved to {self.destination_facility.name}", + shifting_request.comments, + ) + + def test_transfer_with_matching_year_of_birth(self): + # Set the patient's facility to allow transfers + self.patient.allow_transfer = True + self.patient.save() + + self.client.force_authenticate(user=self.super_user) + response = self.client.post( + f"/api/v1/patient/{self.patient.external_id}/transfer/", + { + "year_of_birth": self.patient.year_of_birth, # Correct year of birth + "facility": self.destination_facility.external_id, + }, + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + # Refresh patient data + self.patient.refresh_from_db() + + # Assert the patient's facility has been updated + self.assertEqual(self.patient.facility, self.destination_facility) + + def test_transfer_to_non_existent_facility(self): + self.client.force_authenticate(user=self.super_user) + response = self.client.post( + f"/api/v1/patient/{self.patient.external_id}/transfer/", + { + "year_of_birth": 1992, + "facility": "dff237c5-9410-4714-9101-399941b60ede", # Non-existent facility + }, + ) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + def test_transfer_with_invalid_data(self): + self.client.force_authenticate(user=self.super_user) + response = self.client.post( + f"/api/v1/patient/{self.patient.external_id}/transfer/", + { + "year_of_birth": "invalid-year", # Invalid year of birth + "facility": self.destination_facility.external_id, + }, + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertIn("year_of_birth", response.data) + + def test_unauthorized_transfer_request(self): + # Not authenticating the user to test unauthorized access + response = self.client.post( + f"/api/v1/patient/{self.patient.external_id}/transfer/", + { + "year_of_birth": 1992, + "facility": self.destination_facility.external_id, + }, + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + class PatientSearchTestCase(TestUtils, APITestCase): @classmethod From 5b2b30404dfb75429a47bc8fddc50da9c7b30963 Mon Sep 17 00:00:00 2001 From: Prafful Sharma <115104695+DraKen0009@users.noreply.github.com> Date: Thu, 14 Nov 2024 19:46:30 +0530 Subject: [PATCH 3/9] Clean up asset classes (#2494) Clean up asset classes (#2494) --------- Co-authored-by: Aakash Singh --- care/facility/api/viewsets/asset.py | 4 +- care/facility/tests/test_asset_api.py | 141 ++++++++++++++++++++++ care/utils/assetintegration/base.py | 38 ++++-- care/utils/assetintegration/hl7monitor.py | 10 +- care/utils/assetintegration/onvif.py | 20 +-- care/utils/assetintegration/schema.py | 34 ++++++ care/utils/assetintegration/ventilator.py | 10 +- 7 files changed, 227 insertions(+), 30 deletions(-) create mode 100644 care/utils/assetintegration/schema.py diff --git a/care/facility/api/viewsets/asset.py b/care/facility/api/viewsets/asset.py index 8b24bebb51..caf784cb9f 100644 --- a/care/facility/api/viewsets/asset.py +++ b/care/facility/api/viewsets/asset.py @@ -61,6 +61,7 @@ from care.facility.models.bed import AssetBed, ConsultationBed from care.users.models import User from care.utils.assetintegration.asset_classes import AssetClasses +from care.utils.assetintegration.base import BaseAssetIntegration from care.utils.cache.cache_allowed_facilities import get_accessible_facilities from care.utils.filters.choicefilter import CareChoiceFilter, inverse_choices from care.utils.queryset.asset_bed import get_asset_queryset @@ -389,7 +390,6 @@ def operate_assets(self, request, *args, **kwargs): This API is used to operate assets. API accepts the asset_id and action as parameters. """ try: - action = request.data["action"] asset: Asset = self.get_object() middleware_hostname = ( asset.meta.get( @@ -405,7 +405,7 @@ def operate_assets(self, request, *args, **kwargs): "middleware_hostname": middleware_hostname, } ) - result = asset_class.handle_action(action) + result = asset_class.handle_action(**request.data["action"]) return Response({"result": result}, status=status.HTTP_200_OK) except ValidationError as e: diff --git a/care/facility/tests/test_asset_api.py b/care/facility/tests/test_asset_api.py index a0771b089b..3989a19eef 100644 --- a/care/facility/tests/test_asset_api.py +++ b/care/facility/tests/test_asset_api.py @@ -1,10 +1,14 @@ from django.utils.timezone import now, timedelta from rest_framework import status +from rest_framework.exceptions import ValidationError from rest_framework.test import APITestCase from care.facility.models import Asset, Bed from care.users.models import User from care.utils.assetintegration.asset_classes import AssetClasses +from care.utils.assetintegration.hl7monitor import HL7MonitorAsset +from care.utils.assetintegration.onvif import OnvifAsset +from care.utils.assetintegration.ventilator import VentilatorAsset from care.utils.tests.test_utils import TestUtils @@ -31,6 +35,143 @@ def setUp(self) -> None: super().setUp() self.asset = self.create_asset(self.asset_location) + def validate_invalid_meta(self, asset_class, meta): + with self.assertRaises(ValidationError): + asset_class(meta) + + def test_meta_validations_for_onvif_asset(self): + valid_meta = { + "local_ip_address": "192.168.0.1", + "camera_access_key": "username:password:access_key", + "middleware_hostname": "middleware.local", + "insecure_connection": True, + } + onvif_asset = OnvifAsset(valid_meta) + self.assertEqual(onvif_asset.middleware_hostname, "middleware.local") + self.assertEqual(onvif_asset.host, "192.168.0.1") + self.assertEqual(onvif_asset.username, "username") + self.assertEqual(onvif_asset.password, "password") + self.assertEqual(onvif_asset.access_key, "access_key") + self.assertTrue(onvif_asset.insecure_connection) + + invalid_meta_cases = [ + # Invalid format for camera_access_key + { + "id": "123", + "local_ip_address": "192.168.0.1", + "middleware_hostname": "middleware.local", + "camera_access_key": "invalid_format", + }, + # Missing username/password in camera_access_key + { + "local_ip_address": "192.168.0.1", + "middleware_hostname": "middleware.local", + "camera_access_key": "invalid_format", + }, + # Missing middleware_hostname + { + "local_ip_address": "192.168.0.1", + "camera_access_key": "username:password:access_key", + }, + # Missing local_ip_address + { + "middleware_hostname": "middleware.local", + "camera_access_key": "username:password:access_key", + }, + # Invalid value for insecure_connection + { + "local_ip_address": "192.168.0.1", + "camera_access_key": "username:password:access_key", + "middleware_hostname": "middleware.local", + "insecure_connection": "invalid_value", + }, + ] + for meta in invalid_meta_cases: + self.validate_invalid_meta(OnvifAsset, meta) + + def test_meta_validations_for_ventilator_asset(self): + valid_meta = { + "id": "123", + "local_ip_address": "192.168.0.1", + "middleware_hostname": "middleware.local", + "insecure_connection": True, + } + ventilator_asset = VentilatorAsset(valid_meta) + self.assertEqual(ventilator_asset.middleware_hostname, "middleware.local") + self.assertEqual(ventilator_asset.host, "192.168.0.1") + self.assertTrue(ventilator_asset.insecure_connection) + + invalid_meta_cases = [ + # Missing id + { + "local_ip_address": "192.168.0.1", + "middleware_hostname": "middleware.local", + }, + # Missing middleware_hostname + {"id": "123", "local_ip_address": "192.168.0.1"}, + # Missing local_ip_address + {"id": "123", "middleware_hostname": "middleware.local"}, + # Invalid insecure_connection + { + "id": "123", + "local_ip_address": "192.168.0.1", + "middleware_hostname": "middleware.local", + "insecure_connection": "invalid_value", + }, + # Camera access key not required for ventilator, invalid meta + { + "id": "21", + "local_ip_address": "192.168.0.1", + "camera_access_key": "username:password:access_key", + "middleware_hostname": "middleware.local", + "insecure_connection": True, + }, + ] + for meta in invalid_meta_cases: + self.validate_invalid_meta(VentilatorAsset, meta) + + def test_meta_validations_for_hl7monitor_asset(self): + valid_meta = { + "id": "123", + "local_ip_address": "192.168.0.1", + "middleware_hostname": "middleware.local", + "insecure_connection": True, + } + hl7monitor_asset = HL7MonitorAsset(valid_meta) + self.assertEqual(hl7monitor_asset.middleware_hostname, "middleware.local") + self.assertEqual(hl7monitor_asset.host, "192.168.0.1") + self.assertEqual(hl7monitor_asset.id, "123") + self.assertTrue(hl7monitor_asset.insecure_connection) + + invalid_meta_cases = [ + # Missing id + { + "local_ip_address": "192.168.0.1", + "middleware_hostname": "middleware.local", + }, + # Missing middleware_hostname + {"id": "123", "local_ip_address": "192.168.0.1"}, + # Missing local_ip_address + {"id": "123", "middleware_hostname": "middleware.local"}, + # Invalid insecure_connection + { + "id": "123", + "local_ip_address": "192.168.0.1", + "middleware_hostname": "middleware.local", + "insecure_connection": "invalid_value", + }, + # Camera access key not required for HL7Monitor, invalid meta + { + "id": "123", + "local_ip_address": "192.168.0.1", + "camera_access_key": "username:password:access_key", + "middleware_hostname": "middleware.local", + "insecure_connection": True, + }, + ] + for meta in invalid_meta_cases: + self.validate_invalid_meta(HL7MonitorAsset, meta) + def test_list_assets(self): response = self.client.get("/api/v1/asset/") self.assertEqual(response.status_code, status.HTTP_200_OK) diff --git a/care/utils/assetintegration/base.py b/care/utils/assetintegration/base.py index 334bcecfa5..cc6c59e1c4 100644 --- a/care/utils/assetintegration/base.py +++ b/care/utils/assetintegration/base.py @@ -1,17 +1,35 @@ import json +from typing import TypedDict +import jsonschema import requests from django.conf import settings +from jsonschema import ValidationError as JSONValidationError from rest_framework import status -from rest_framework.exceptions import APIException +from rest_framework.exceptions import APIException, ValidationError from care.utils.jwks.token_generator import generate_jwt +from .schema import meta_object_schema + + +class ActionParams(TypedDict, total=False): + type: str + data: dict | None + timeout: int | None + class BaseAssetIntegration: auth_header_type = "Care_Bearer " def __init__(self, meta): + try: + meta["_name"] = self._name + jsonschema.validate(instance=meta, schema=meta_object_schema) + except JSONValidationError as e: + error_message = f"Invalid metadata: {e.message}" + raise ValidationError(error_message) from e + self.meta = meta self.id = self.meta.get("id", "") self.host = self.meta["local_ip_address"] @@ -19,8 +37,8 @@ def __init__(self, meta): self.insecure_connection = self.meta.get("insecure_connection", False) self.timeout = settings.MIDDLEWARE_REQUEST_TIMEOUT - def handle_action(self, action): - pass + def handle_action(self, **kwargs): + """Handle actions using kwargs instead of dict.""" def get_url(self, endpoint): protocol = "http" @@ -48,16 +66,14 @@ def _validate_response(self, response: requests.Response): {"error": "Invalid Response"}, response.status_code ) from e - def api_post(self, url, data=None): + def api_post(self, url, data=None, timeout=None): + timeout = timeout or self.timeout return self._validate_response( - requests.post( - url, json=data, headers=self.get_headers(), timeout=self.timeout - ) + requests.post(url, json=data, headers=self.get_headers(), timeout=timeout) ) - def api_get(self, url, data=None): + def api_get(self, url, data=None, timeout=None): + timeout = timeout or self.timeout return self._validate_response( - requests.get( - url, params=data, headers=self.get_headers(), timeout=self.timeout - ) + requests.get(url, params=data, headers=self.get_headers(), timeout=timeout) ) diff --git a/care/utils/assetintegration/hl7monitor.py b/care/utils/assetintegration/hl7monitor.py index abd14247d3..bf331f71ca 100644 --- a/care/utils/assetintegration/hl7monitor.py +++ b/care/utils/assetintegration/hl7monitor.py @@ -2,7 +2,7 @@ from rest_framework.exceptions import ValidationError -from care.utils.assetintegration.base import BaseAssetIntegration +from care.utils.assetintegration.base import ActionParams, BaseAssetIntegration class HL7MonitorAsset(BaseAssetIntegration): @@ -20,12 +20,13 @@ def __init__(self, meta): {key: f"{key} not found in asset metadata" for key in e.args} ) from e - def handle_action(self, action): - action_type = action["type"] + def handle_action(self, **kwargs: ActionParams): + action_type = kwargs["type"] + timeout = kwargs.get("timeout") if action_type == self.HL7MonitorActions.GET_VITALS.value: request_params = {"device_id": self.host} - return self.api_get(self.get_url("vitals"), request_params) + return self.api_get(self.get_url("vitals"), request_params, timeout) if action_type == self.HL7MonitorActions.GET_STREAM_TOKEN.value: return self.api_post( @@ -34,6 +35,7 @@ def handle_action(self, action): "asset_id": self.id, "ip": self.host, }, + timeout, ) raise ValidationError({"action": "invalid action type"}) diff --git a/care/utils/assetintegration/onvif.py b/care/utils/assetintegration/onvif.py index 815994855e..2dd814b4e6 100644 --- a/care/utils/assetintegration/onvif.py +++ b/care/utils/assetintegration/onvif.py @@ -2,7 +2,7 @@ from rest_framework.exceptions import ValidationError -from care.utils.assetintegration.base import BaseAssetIntegration +from care.utils.assetintegration.base import ActionParams, BaseAssetIntegration class OnvifAsset(BaseAssetIntegration): @@ -27,9 +27,10 @@ def __init__(self, meta): {key: f"{key} not found in asset metadata" for key in e.args} ) from e - def handle_action(self, action): - action_type = action["type"] - action_data = action.get("data", {}) + def handle_action(self, **kwargs: ActionParams): + action_type = kwargs["type"] + action_data = kwargs.get("data", {}) + timeout = kwargs.get("timeout") request_body = { "hostname": self.host, @@ -41,19 +42,19 @@ def handle_action(self, action): } if action_type == self.OnvifActions.GET_CAMERA_STATUS.value: - return self.api_get(self.get_url("status"), request_body) + return self.api_get(self.get_url("status"), request_body, timeout) if action_type == self.OnvifActions.GET_PRESETS.value: - return self.api_get(self.get_url("presets"), request_body) + return self.api_get(self.get_url("presets"), request_body, timeout) if action_type == self.OnvifActions.GOTO_PRESET.value: - return self.api_post(self.get_url("gotoPreset"), request_body) + return self.api_post(self.get_url("gotoPreset"), request_body, timeout) if action_type == self.OnvifActions.ABSOLUTE_MOVE.value: - return self.api_post(self.get_url("absoluteMove"), request_body) + return self.api_post(self.get_url("absoluteMove"), request_body, timeout) if action_type == self.OnvifActions.RELATIVE_MOVE.value: - return self.api_post(self.get_url("relativeMove"), request_body) + return self.api_post(self.get_url("relativeMove"), request_body, timeout) if action_type == self.OnvifActions.GET_STREAM_TOKEN.value: return self.api_post( @@ -61,6 +62,7 @@ def handle_action(self, action): { "stream_id": self.access_key, }, + timeout, ) raise ValidationError({"action": "invalid action type"}) diff --git a/care/utils/assetintegration/schema.py b/care/utils/assetintegration/schema.py new file mode 100644 index 0000000000..3396747162 --- /dev/null +++ b/care/utils/assetintegration/schema.py @@ -0,0 +1,34 @@ +meta_object_schema = { + "type": "object", + "properties": { + "id": {"type": "string"}, + "local_ip_address": {"type": "string", "format": "ipv4"}, + "middleware_hostname": {"type": "string"}, + "insecure_connection": {"type": "boolean", "default": False}, + "camera_access_key": { + "type": "string", + "pattern": "^[^:]+:[^:]+:[^:]+$", # valid pattern for "abc:def:ghi" , "123:456:789" + }, + }, + "required": ["local_ip_address", "middleware_hostname"], + "allOf": [ + { + "if": {"properties": {"_name": {"const": "onvif"}}}, + "then": { + "properties": {"camera_access_key": {"type": "string"}}, + "required": [ + "camera_access_key" + ], # Require camera_access_key for Onvif + }, + "else": { + "properties": {"id": {"type": "string"}}, + "required": ["id"], # Require id for non-Onvif assets + "not": { + "required": [ + "camera_access_key" + ] # Make camera_access_key not required for non-Onvif + }, + }, + } + ], +} diff --git a/care/utils/assetintegration/ventilator.py b/care/utils/assetintegration/ventilator.py index 23a5280960..afb896bfdb 100644 --- a/care/utils/assetintegration/ventilator.py +++ b/care/utils/assetintegration/ventilator.py @@ -2,7 +2,7 @@ from rest_framework.exceptions import ValidationError -from care.utils.assetintegration.base import BaseAssetIntegration +from care.utils.assetintegration.base import ActionParams, BaseAssetIntegration class VentilatorAsset(BaseAssetIntegration): @@ -20,12 +20,13 @@ def __init__(self, meta): {key: f"{key} not found in asset metadata" for key in e.args} ) from e - def handle_action(self, action): - action_type = action["type"] + def handle_action(self, **kwargs: ActionParams): + action_type = kwargs["type"] + timeout = kwargs.get("timeout") if action_type == self.VentilatorActions.GET_VITALS.value: request_params = {"device_id": self.host} - return self.api_get(self.get_url("vitals"), request_params) + return self.api_get(self.get_url("vitals"), request_params, timeout) if action_type == self.VentilatorActions.GET_STREAM_TOKEN.value: return self.api_post( @@ -34,6 +35,7 @@ def handle_action(self, action): "asset_id": self.id, "ip": self.host, }, + timeout, ) raise ValidationError({"action": "invalid action type"}) From 7986fdd3696e57378566120b92f1eb3b267a81b5 Mon Sep 17 00:00:00 2001 From: "qodana-cloud[bot]" <163413896+qodana-cloud[bot]@users.noreply.github.com> Date: Wed, 20 Nov 2024 14:08:30 +0530 Subject: [PATCH 4/9] Add qodana CI checks (#2607) * Add qodana.yaml file * Add github workflow file * Update qodana_code_quality.yml --------- Co-authored-by: Qodana Application Co-authored-by: Vignesh Hari <14056798+vigneshhari@users.noreply.github.com> --- .github/workflows/qodana_code_quality.yml | 27 +++++++++++++++++++++++ qodana.yaml | 6 +++++ 2 files changed, 33 insertions(+) create mode 100644 .github/workflows/qodana_code_quality.yml create mode 100644 qodana.yaml diff --git a/.github/workflows/qodana_code_quality.yml b/.github/workflows/qodana_code_quality.yml new file mode 100644 index 0000000000..66640a50c9 --- /dev/null +++ b/.github/workflows/qodana_code_quality.yml @@ -0,0 +1,27 @@ +name: Qodana +on: + workflow_dispatch: + pull_request: + push: + branches: # Specify your branches here + - develop + +jobs: + qodana: + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + checks: write + steps: + - uses: actions/checkout@v3 + with: + ref: ${{ github.event.pull_request.head.sha }} # to check out the actual pull request commit, not the merge commit + fetch-depth: 0 # a full history is required for pull request analysis + - name: 'Qodana Scan' + uses: JetBrains/qodana-action@v2024.2 + with: + pr-mode: false + env: + QODANA_TOKEN: ${{ secrets.QODANA_TOKEN_1210838162 }} + QODANA_ENDPOINT: 'https://qodana.cloud' diff --git a/qodana.yaml b/qodana.yaml new file mode 100644 index 0000000000..c2af26bad6 --- /dev/null +++ b/qodana.yaml @@ -0,0 +1,6 @@ +version: "1.0" +linter: jetbrains/qodana-python:2024.2 +profile: + name: qodana.recommended +include: + - name: CheckDependencyLicenses \ No newline at end of file From 7114ba88a506cc7cce20b14b632b8e7e5e7ae209 Mon Sep 17 00:00:00 2001 From: vigneshhari Date: Wed, 20 Nov 2024 14:17:08 +0530 Subject: [PATCH 5/9] Fix linting issue --- qodana.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qodana.yaml b/qodana.yaml index c2af26bad6..368475bc3e 100644 --- a/qodana.yaml +++ b/qodana.yaml @@ -3,4 +3,4 @@ linter: jetbrains/qodana-python:2024.2 profile: name: qodana.recommended include: - - name: CheckDependencyLicenses \ No newline at end of file + - name: CheckDependencyLicenses From 734c3e068602ca50bfdca0581ced7db3ff461299 Mon Sep 17 00:00:00 2001 From: Mohammed Nihal <57055998+nihal467@users.noreply.github.com> Date: Wed, 20 Nov 2024 15:48:10 +0530 Subject: [PATCH 6/9] Modified the Patient and users names in dummy data (#2608) modified the users and patient names --- data/dummy/facility.json | 34 +++++++++++++++++----------------- data/dummy/users.json | 6 +++--- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/data/dummy/facility.json b/data/dummy/facility.json index 17d98574ff..c6875988db 100644 --- a/data/dummy/facility.json +++ b/data/dummy/facility.json @@ -4164,7 +4164,7 @@ "facility": 1, "nearest_facility": null, "meta_info": null, - "name": "Dummy Patient", + "name": "Dummy Patient One", "gender": 1, "phone_number": "+919987455444", "emergency_phone_number": "+919898797775", @@ -4247,7 +4247,7 @@ "facility": 1, "nearest_facility": null, "meta_info": null, - "name": "Test E2E User", + "name": "Dummy Patient Two", "gender": 1, "phone_number": "+919765259927", "emergency_phone_number": "+919228973557", @@ -4330,7 +4330,7 @@ "facility": 1, "nearest_facility": null, "meta_info": null, - "name": "Dummy Patient 1", + "name": "Dummy Patient Three", "gender": 1, "phone_number": "+919192495353", "emergency_phone_number": "+919460491040", @@ -4413,7 +4413,7 @@ "facility": 1, "nearest_facility": null, "meta_info": null, - "name": "Dummy Patient 2", + "name": "Dummy Patient Four", "gender": 1, "phone_number": "+919112608904", "emergency_phone_number": "+919110616234", @@ -4496,7 +4496,7 @@ "facility": 1, "nearest_facility": null, "meta_info": null, - "name": "Dummy Patient 3", + "name": "Dummy Patient Five", "gender": 1, "phone_number": "+919640229897", "emergency_phone_number": "+919135436547", @@ -4579,7 +4579,7 @@ "facility": 1, "nearest_facility": null, "meta_info": null, - "name": "Dummy Patient 4", + "name": "Dummy Patient Six", "gender": 1, "phone_number": "+919762277015", "emergency_phone_number": "+919342634016", @@ -4662,7 +4662,7 @@ "facility": 1, "nearest_facility": null, "meta_info": null, - "name": "Dummy Patient 5", + "name": "Dummy Patient Seven", "gender": 1, "phone_number": "+919303212282", "emergency_phone_number": "+919229738916", @@ -4745,7 +4745,7 @@ "facility": 1, "nearest_facility": null, "meta_info": null, - "name": "Dummy Patient 6", + "name": "Dummy Patient Eight", "gender": 1, "phone_number": "+919740701377", "emergency_phone_number": "+919321666516", @@ -4828,7 +4828,7 @@ "facility": 1, "nearest_facility": null, "meta_info": null, - "name": "Dummy Patient 7", + "name": "Dummy Patient Nine", "gender": 1, "phone_number": "+919148299129", "emergency_phone_number": "+919267280161", @@ -4911,7 +4911,7 @@ "facility": 1, "nearest_facility": null, "meta_info": null, - "name": "Dummy Patient 8", + "name": "Dummy Patient Ten", "gender": 1, "phone_number": "+919490490290", "emergency_phone_number": "+919828674710", @@ -4994,7 +4994,7 @@ "facility": 1, "nearest_facility": null, "meta_info": null, - "name": "Dummy Patient 9", + "name": "Dummy Patient Eleven", "gender": 1, "phone_number": "+919983927490", "emergency_phone_number": "+919781111140", @@ -5160,7 +5160,7 @@ "facility": 1, "nearest_facility": null, "meta_info": null, - "name": "Dummy Patient 11", + "name": "Dummy Patient Twelve", "gender": 1, "phone_number": "+919343556704", "emergency_phone_number": "+919967920474", @@ -5243,7 +5243,7 @@ "facility": 1, "nearest_facility": null, "meta_info": null, - "name": "Dummy Patient 12", + "name": "Dummy Patient Thirteen", "gender": 1, "phone_number": "+919320374643", "emergency_phone_number": "+919493558024", @@ -5326,7 +5326,7 @@ "facility": 1, "nearest_facility": null, "meta_info": null, - "name": "Dummy Patient 13", + "name": "Discharge Patient One", "gender": 1, "phone_number": "+919292990239", "emergency_phone_number": "+919992258784", @@ -5409,7 +5409,7 @@ "facility": 1, "nearest_facility": null, "meta_info": null, - "name": "Dummy Patient 14", + "name": "Discharge Patient Two", "gender": 1, "phone_number": "+919650206292", "emergency_phone_number": "+919596454242", @@ -5492,7 +5492,7 @@ "facility": 1, "nearest_facility": null, "meta_info": null, - "name": "Dummy Patient 15", + "name": "Discharge Patient Three", "gender": 1, "phone_number": "+919266236581", "emergency_phone_number": "+919835286558", @@ -5575,7 +5575,7 @@ "facility": 1, "nearest_facility": null, "meta_info": null, - "name": "Dummy Patient 16", + "name": "Discharge Patient Four", "gender": 1, "phone_number": "+919243083817", "emergency_phone_number": "+919924971004", diff --git a/data/dummy/users.json b/data/dummy/users.json index e7b0115614..b86ae90ac4 100644 --- a/data/dummy/users.json +++ b/data/dummy/users.json @@ -894,14 +894,14 @@ "password": "argon2$argon2id$v=19$m=102400,t=2,p=8$bUNTR1MwejJYNXdXd2VUYjJHMmN5bw$alS6S9Ay3bvIHe9U18luyn7LyVaArgrgHIt+vh4ta48", "last_login": null, "is_superuser": false, - "first_name": "Dev", - "last_name": "Doctor Two", + "first_name": "Tester", + "last_name": "Doctor", "email": "devdoctor1@test.com", "is_staff": false, "is_active": true, "date_joined": "2024-10-14T07:53:32.400Z", "external_id": "009c4fc2-f7af-4a02-9383-6fbb4af2fdbb", - "username": "devdoctor1", + "username": "dev-doctor2", "user_type": 15, "created_by": 2, "ward": null, From e90133344e13e21f8dec2c9f99cb052414c4bdc5 Mon Sep 17 00:00:00 2001 From: Mohammed Nihal <57055998+nihal467@users.noreply.github.com> Date: Wed, 20 Nov 2024 17:43:48 +0530 Subject: [PATCH 7/9] Modified Missed Patient Name in dummy data (#2609) * modified the users and patient names * missed out one patient name * missed out one patient name --- data/dummy/facility.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/data/dummy/facility.json b/data/dummy/facility.json index c6875988db..da4b726469 100644 --- a/data/dummy/facility.json +++ b/data/dummy/facility.json @@ -5077,7 +5077,7 @@ "facility": 1, "nearest_facility": null, "meta_info": null, - "name": "Dummy Patient 10", + "name": "Dummy Patient Twelve", "gender": 1, "phone_number": "+919849511866", "emergency_phone_number": "+919622326248", @@ -5160,7 +5160,7 @@ "facility": 1, "nearest_facility": null, "meta_info": null, - "name": "Dummy Patient Twelve", + "name": "Dummy Patient Thirteen", "gender": 1, "phone_number": "+919343556704", "emergency_phone_number": "+919967920474", @@ -5243,7 +5243,7 @@ "facility": 1, "nearest_facility": null, "meta_info": null, - "name": "Dummy Patient Thirteen", + "name": "Dummy Patient Fourteen", "gender": 1, "phone_number": "+919320374643", "emergency_phone_number": "+919493558024", From 2970fe1f4a95a300fedd1df427a48a080fe2d56f Mon Sep 17 00:00:00 2001 From: Aakash Singh Date: Thu, 21 Nov 2024 19:40:49 +0530 Subject: [PATCH 8/9] fix asset class initialization (#2611) --- care/facility/api/viewsets/asset.py | 2 +- care/facility/tasks/asset_monitor.py | 2 +- care/facility/tests/test_asset_api.py | 21 +++++++++++++++++++++ 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/care/facility/api/viewsets/asset.py b/care/facility/api/viewsets/asset.py index caf784cb9f..2563dfaf45 100644 --- a/care/facility/api/viewsets/asset.py +++ b/care/facility/api/viewsets/asset.py @@ -401,7 +401,7 @@ def operate_assets(self, request, *args, **kwargs): asset_class: BaseAssetIntegration = AssetClasses[asset.asset_class].value( { **asset.meta, - "id": asset.external_id, + "id": str(asset.external_id), "middleware_hostname": middleware_hostname, } ) diff --git a/care/facility/tasks/asset_monitor.py b/care/facility/tasks/asset_monitor.py index 9c8701618c..1adfa725ae 100644 --- a/care/facility/tasks/asset_monitor.py +++ b/care/facility/tasks/asset_monitor.py @@ -65,7 +65,7 @@ def check_asset_status(): # noqa: PLR0912 ].value( { **asset.meta, - "id": asset.external_id, + "id": str(asset.external_id), "middleware_hostname": resolved_middleware, } ) diff --git a/care/facility/tests/test_asset_api.py b/care/facility/tests/test_asset_api.py index 3989a19eef..0e96498d4b 100644 --- a/care/facility/tests/test_asset_api.py +++ b/care/facility/tests/test_asset_api.py @@ -6,6 +6,7 @@ from care.facility.models import Asset, Bed from care.users.models import User from care.utils.assetintegration.asset_classes import AssetClasses +from care.utils.assetintegration.base import BaseAssetIntegration from care.utils.assetintegration.hl7monitor import HL7MonitorAsset from care.utils.assetintegration.onvif import OnvifAsset from care.utils.assetintegration.ventilator import VentilatorAsset @@ -39,6 +40,26 @@ def validate_invalid_meta(self, asset_class, meta): with self.assertRaises(ValidationError): asset_class(meta) + def test_asset_class_initialization(self): + asset = self.create_asset( + self.asset_location, + asset_class=AssetClasses.ONVIF.name, + meta={ + "local_ip_address": "192.168.0.1", + "camera_access_key": "username:password:access_key", + "middleware_hostname": "middleware.local", + "insecure_connection": True, + }, + ) + asset_class = AssetClasses[asset.asset_class].value( + { + **asset.meta, + "id": str(asset.external_id), + "middleware_hostname": "middleware.local", + } + ) + self.assertIsInstance(asset_class, BaseAssetIntegration) + def test_meta_validations_for_onvif_asset(self): valid_meta = { "local_ip_address": "192.168.0.1", From d23cbcb8461c9e5c545eca045597a201fa252cf6 Mon Sep 17 00:00:00 2001 From: Aakash Singh Date: Thu, 21 Nov 2024 20:02:33 +0530 Subject: [PATCH 9/9] Revert "Add qodana CI checks (#2607)" (#2612) This reverts commit 7986fdd3696e57378566120b92f1eb3b267a81b5. --- .github/workflows/qodana_code_quality.yml | 27 ----------------------- qodana.yaml | 6 ----- 2 files changed, 33 deletions(-) delete mode 100644 .github/workflows/qodana_code_quality.yml delete mode 100644 qodana.yaml diff --git a/.github/workflows/qodana_code_quality.yml b/.github/workflows/qodana_code_quality.yml deleted file mode 100644 index 66640a50c9..0000000000 --- a/.github/workflows/qodana_code_quality.yml +++ /dev/null @@ -1,27 +0,0 @@ -name: Qodana -on: - workflow_dispatch: - pull_request: - push: - branches: # Specify your branches here - - develop - -jobs: - qodana: - runs-on: ubuntu-latest - permissions: - contents: write - pull-requests: write - checks: write - steps: - - uses: actions/checkout@v3 - with: - ref: ${{ github.event.pull_request.head.sha }} # to check out the actual pull request commit, not the merge commit - fetch-depth: 0 # a full history is required for pull request analysis - - name: 'Qodana Scan' - uses: JetBrains/qodana-action@v2024.2 - with: - pr-mode: false - env: - QODANA_TOKEN: ${{ secrets.QODANA_TOKEN_1210838162 }} - QODANA_ENDPOINT: 'https://qodana.cloud' diff --git a/qodana.yaml b/qodana.yaml deleted file mode 100644 index 368475bc3e..0000000000 --- a/qodana.yaml +++ /dev/null @@ -1,6 +0,0 @@ -version: "1.0" -linter: jetbrains/qodana-python:2024.2 -profile: - name: qodana.recommended -include: - - name: CheckDependencyLicenses