Repository: smart-on-fhir/client-py

Issue:
FHIRClient patient property causing exception
This import was not updated to relative import 
https://github.com/smart-on-fhir/client-py/blob/main/fhirclient/client.py#L174
attempt to use `patient` property raise ModuleNotFound exception

Discussion:
@mikix: Thanks for the report! That slipped through the cracks. Added new unit tests for that section of code, and released 4.2.1

Solution patch:
diff --git a/fhirclient/client.py b/fhirclient/client.py
index f492a6df..d6d12de4 100644
--- a/fhirclient/client.py
+++ b/fhirclient/client.py
@@ -2,7 +2,7 @@ import logging
 
 from .server import FHIRServer, FHIRUnauthorizedException, FHIRNotFoundException
 
-__version__ = '4.2.0'
+__version__ = '4.2.1'
 __author__ = 'SMART Platforms Team'
 __license__ = 'APACHE2'
 __copyright__ = "Copyright 2017 Boston Children's Hospital"
@@ -166,16 +166,16 @@ class FHIRClient(object):
     @property
     def patient(self):
         if self._patient is None and self.patient_id is not None and self.ready:
-            import models.patient
+            from fhirclient.models.patient import Patient
             try:
                 logger.debug("SMART: Attempting to read Patient {0}".format(self.patient_id))
-                self._patient = models.patient.Patient.read(self.patient_id, self.server)
-            except FHIRUnauthorizedException as e:
+                self._patient = Patient.read(self.patient_id, self.server)
+            except FHIRUnauthorizedException:
                 if self.reauthorize():
                     logger.debug("SMART: Attempting to read Patient {0} after reauthorizing"
                         .format(self.patient_id))
-                    self._patient = models.patient.Patient.read(self.patient_id, self.server)
-            except FHIRNotFoundException as e:
+                    self._patient = Patient.read(self.patient_id, self.server)
+            except FHIRNotFoundException:
                 logger.warning("SMART: Patient with id {0} not found".format(self.patient_id))
                 self.patient_id = None
             self.save_state()


Test patch:
diff --git a/tests/client_test.py b/tests/client_test.py
index be131563..21d0fdb8 100644
--- a/tests/client_test.py
+++ b/tests/client_test.py
@@ -1,6 +1,13 @@
 import unittest
+from unittest import mock
 
 from fhirclient.client import FHIRClient
+from fhirclient.server import FHIRNotFoundException, FHIRUnauthorizedException
+
+# Smallest valid-but-fake client state
+MIN_STATE = {
+    "server": {"base_uri": "http://example.com/fhir"},
+}
 
 
 class TestClient(unittest.TestCase):
@@ -39,3 +46,54 @@ class TestClient(unittest.TestCase):
         client.from_state({'app_id': 'NewID', 'server': state['server']})
         self.assertEqual('NewID', client.app_id)
         self.assertEqual('LaunchToken', client.launch_token)
+
+    @mock.patch("fhirclient.models.patient.Patient.read")
+    def test_patient_property_happy_path(self, mock_read):
+        save_func = mock.MagicMock()
+
+        # Verify that we gracefully handle no patient_id being given
+        client = FHIRClient(state=MIN_STATE, save_func=save_func)
+        self.assertIsNone(client.patient)
+        self.assertEqual(mock_read.call_count, 0)
+        self.assertEqual(save_func.call_count, 0)
+
+        # Verify we expose the provided patient ID as a Patient object
+        client = FHIRClient(state={"patient_id": "P123", **MIN_STATE}, save_func=save_func)
+        self.assertIsNotNone(client.patient)
+        self.assertEqual(mock_read.call_count, 1)
+        self.assertEqual(mock_read.call_args, mock.call("P123", client.server))
+        self.assertEqual(save_func.call_count, 1)
+
+    @mock.patch("fhirclient.models.patient.Patient.read")
+    @mock.patch("fhirclient.client.FHIRClient.reauthorize")
+    def test_patient_property_unauthorized(self, mock_reauthorize, mock_read):
+        """We should attempt to reauthorize and re-request the patient"""
+
+        client = FHIRClient(state={"patient_id": "P123", **MIN_STATE})
+
+        # First try with a failed re-authorize
+        mock_read.side_effect = FHIRUnauthorizedException("response")
+        mock_reauthorize.return_value = False
+        self.assertIsNone(client.patient)
+        self.assertEqual(mock_read.call_count, 1)
+        self.assertEqual(mock_reauthorize.call_count, 1)
+
+        # Then with a successful re-authorize
+        mock_read.reset_mock()
+        mock_read.side_effect = [FHIRUnauthorizedException("response"), mock.MagicMock()]
+        mock_reauthorize.reset_mock()
+        mock_reauthorize.return_value = True
+        self.assertIsNotNone(client.patient)
+        self.assertEqual(mock_read.call_count, 2)
+        self.assertEqual(mock_reauthorize.call_count, 1)
+
+    @mock.patch("fhirclient.models.patient.Patient.read")
+    def test_patient_property_not_found(self, mock_read):
+        """We should attempt to reauthorize and re-request the patient"""
+        mock_read.side_effect = FHIRNotFoundException("response")
+
+        client = FHIRClient(state={"patient_id": "P123", **MIN_STATE})
+        self.assertEqual(client.patient_id, "P123")  # sanity check before we start
+
+        self.assertIsNone(client.patient)
+        self.assertIsNone(client.patient_id)  # we clear out the patient id