Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion firebase_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
45 changes: 45 additions & 0 deletions tests/test_firebase_utils.py
Original file line number Diff line number Diff line change
@@ -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()
Comment on lines +1 to +45

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Manipulating sys.modules by deleting and reloading modules during tests can lead to unexpected side effects, such as breaking other tests or causing issues with modules that have already imported firebase_utils (e.g., action_devices).

Instead of reloading the module, you can use unittest.mock.patch to directly mock firebase_utils.FIREBASE_AVAILABLE and firebase_utils.db (using create=True to handle cases where db is not defined due to a missing firebase_admin installation). This is much cleaner, more idiomatic, and avoids test isolation issues.

import unittest
from unittest.mock import patch, MagicMock
import firebase_utils

class TestFirebaseUtils(unittest.TestCase):
    @patch('firebase_utils.FIREBASE_AVAILABLE', True)
    @patch('firebase_utils.db', create=True)
    def test_reference_value_error_fallback(self, mock_db):
        mock_db.reference.side_effect = ValueError("The default Firebase app does not exist.")

        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])

    @patch('firebase_utils.FIREBASE_AVAILABLE', True)
    @patch('firebase_utils.db', create=True)
    def test_reference_other_exception_bubbles_up(self, mock_db):
        mock_db.reference.side_effect = PermissionError("Permission denied.")

        with self.assertRaises(PermissionError):
            firebase_utils.reference()

if __name__ == '__main__':
    unittest.main()

33 changes: 13 additions & 20 deletions tests/test_multi_tenant.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"}
}
Expand All @@ -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",
Expand All @@ -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")
Expand All @@ -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)
Expand Down