diff --git a/firebase_utils.py b/firebase_utils.py index cb20d1a..b99c9cf 100644 --- a/firebase_utils.py +++ b/firebase_utils.py @@ -93,7 +93,7 @@ def reference(user_id=None): if user_scope: return db.reference(f'/users/{user_scope}/devices') return db.reference('/devices') - except Exception as e: + except ValueError as e: # Firebase is installed but not initialized (e.g. missing credentials in dev) logger.warning("Firebase not initialized, falling back to mock data: %s", e) return MockRef() diff --git a/tests/test_firebase_utils.py b/tests/test_firebase_utils.py new file mode 100644 index 0000000..3045435 --- /dev/null +++ b/tests/test_firebase_utils.py @@ -0,0 +1,45 @@ +import unittest +from unittest.mock import patch, MagicMock + +class TestFirebaseUtils(unittest.TestCase): + def test_reference_value_error_fallback(self): + # We must clear the module to reload it with our mocked FIREBASE_AVAILABLE state + import sys + if 'firebase_utils' in sys.modules: + del sys.modules['firebase_utils'] + + # Mock firebase_admin and db to simulate an uninitialized state where db.reference raises ValueError + mock_db = MagicMock() + mock_db.reference.side_effect = ValueError("The default Firebase app does not exist.") + + with patch.dict(sys.modules, {'firebase_admin': MagicMock(db=mock_db)}): + import firebase_utils + + # FIREBASE_AVAILABLE should be True because the import succeeds (mocked) + self.assertTrue(firebase_utils.FIREBASE_AVAILABLE) + + with patch('firebase_utils.logger.warning') as mock_logger: + ref = firebase_utils.reference() + + # Should fallback to MockRef + self.assertIsInstance(ref, firebase_utils.MockRef) + mock_logger.assert_called_once() + self.assertIn("Firebase not initialized", mock_logger.call_args[0][0]) + + def test_reference_other_exception_bubbles_up(self): + import sys + if 'firebase_utils' in sys.modules: + del sys.modules['firebase_utils'] + + mock_db = MagicMock() + # Some other error like network permission denied + mock_db.reference.side_effect = PermissionError("Permission denied.") + + with patch.dict(sys.modules, {'firebase_admin': MagicMock(db=mock_db)}): + import firebase_utils + + with self.assertRaises(PermissionError): + firebase_utils.reference() + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_multi_tenant.py b/tests/test_multi_tenant.py index 308bf88..de9e937 100644 --- a/tests/test_multi_tenant.py +++ b/tests/test_multi_tenant.py @@ -19,12 +19,12 @@ def setUp(self): def tearDown(self): self.ctx.pop() - @patch('firebase_utils.db') + @patch('action_devices.reference') @patch('firebase_utils.FIREBASE_AVAILABLE', True) - def test_on_sync_scoped_path(self, mock_db): + def test_on_sync_scoped_path(self, mock_reference): # Mock Firebase response for user 123 mock_ref = MagicMock() - mock_db.reference.return_value = mock_ref + mock_reference.return_value = mock_ref mock_ref.get.return_value = { "device1": {"name": {"name": "Light 1"}, "type": "light"} } @@ -33,15 +33,15 @@ def test_on_sync_scoped_path(self, mock_db): response = onSync(user_id="123") # Verify correct Firebase path was hit - mock_db.reference.assert_called_with('/users/123/devices') + mock_reference.assert_called_with('123') self.assertEqual(response['agentUserId'], "123") self.assertEqual(len(response['devices']), 1) self.assertEqual(response['devices'][0]['id'], "device1") @patch('action_devices.mqtt') - @patch('firebase_utils.db') + @patch('action_devices.reference') @patch('firebase_utils.FIREBASE_AVAILABLE', True) - def test_on_execute_mqtt_topic(self, mock_db, mock_mqtt): + def test_on_execute_mqtt_topic(self, mock_reference, mock_mqtt): # Mock request for user 456 req = { "requestId": "req1", @@ -61,7 +61,7 @@ def test_on_execute_mqtt_topic(self, mock_db, mock_mqtt): # Mock Firebase update mock_ref = MagicMock() - mock_db.reference.return_value = mock_ref + mock_reference.return_value = mock_ref # Call actions actions(req, user_id="456") @@ -72,31 +72,24 @@ def test_on_execute_mqtt_topic(self, mock_db, mock_mqtt): call_args = mock_mqtt.publish.call_args self.assertEqual(call_args.kwargs['topic'], expected_topic) - @patch('firebase_utils.db') + @patch('notifications._get_user_device_states_ref') @patch('firebase_utils.FIREBASE_AVAILABLE', True) - def test_mqtt_status_update_firebase_path(self, mock_db): + def test_mqtt_status_update_firebase_path(self, mock_reference): # Mock MQTT message: user 789 reports status for lamp1 mock_message = MagicMock() mock_message.topic = "789/lamp1/status" mock_message.payload = b'{"on": false, "online": true}' # Mock reference chain - mock_user_ref = MagicMock() - mock_device_ref = MagicMock() - mock_states_ref = MagicMock() - - mock_db.reference.return_value = mock_user_ref - mock_user_ref.child.return_value = mock_device_ref - mock_device_ref.child.return_value = mock_states_ref + # Call handle_messages handle_messages(None, None, mock_message) # Verify Firebase update path is scoped correctly - mock_db.reference.assert_called_with('/users/789/devices') - mock_user_ref.child.assert_called_with('lamp1') - mock_device_ref.child.assert_called_with('states') - mock_states_ref.update.assert_called_with({"on": False, "online": True}) + mock_reference.assert_called_with('789', 'lamp1') + + mock_reference.return_value.update.assert_called_with({"on": False, "online": True}) def test_mqtt_log_filtering(self): # Clear logs (since they are in-memory deque)