import asyncio from threading import Thread from unittest.mock import PropertyMock, create_autospec, MagicMock from time import sleep import pytest from .....src.config import SNOWFLAKE_CLIENT_SETTINGS from .....src.connectors.snowflake_db import client SnowflakeConnection = client.sf_connector.SnowflakeConnection CONCURRENCY_COUNT: int = 10 class TestClient: _class = client.Client @pytest.fixture def instance(self): """Create a Snowflake instance.""" return self._class(**SNOWFLAKE_CLIENT_SETTINGS) @pytest.fixture def instance_mocked_connection(self, instance, mock_connection): """Return an instance with a mocked connection.""" instance.connection = mock_connection return instance @pytest.fixture def mock_connection(self): """Return a mock connection.""" return create_autospec(SnowflakeConnection) @pytest.fixture def patcher_connect(self, mocker, mock_connection): return mocker.patch.object( client.sf_connector, "connect", return_value=mock_connection ) @pytest.fixture def patcher_disconnect(self, mocker): return mocker.patch.object(self._class, "disconnect", return_value=None) @pytest.fixture def patcher_private_key(self, mocker, instance): return mocker.patch.object( self._class, "private_key", new_callable=PropertyMock ) def test_init(self, instance): assert instance.connection is None def test_private_key_not_provided(self, instance): with pytest.raises(ValueError): _ = instance.private_key def test_private_key_already_set(self, mocker, instance): instance._private_key_content = mocker.MagicMock() assert ( instance.private_key == instance._private_key_content ), "The cached private key content should be returned." @pytest.mark.asyncio async def test_cursor(self, instance_mocked_connection): await instance_mocked_connection.get_cursor() assert instance_mocked_connection.connection.cursor.call_count == 1 @pytest.mark.asyncio async def test_connect(self, instance, patcher_connect, patcher_private_key): connections = (instance.connect() for _ in range(CONCURRENCY_COUNT)) await asyncio.gather(*connections) assert patcher_private_key.call_count == 1 assert patcher_connect.call_count == 1 assert not patcher_connect.call_args.kwargs.get( "insecure_mode" ), "Insecure mode should not be used in production." assert isinstance( instance.connection, SnowflakeConnection, ), "Connection was not correctly persisted." @pytest.mark.asyncio async def test_connect_log_periodic_message( self, mocker, instance, patcher_connect, patcher_private_key ): mock_log_periodic_message = mocker.spy(instance, "_log_periodic_message") instance.connection = None patcher_connect.return_value = None # Simulate a connection which takes enough time to be established # so that a periodic message can be emitted in between. def connect_side_effect(**_): sleep(0.002) patcher_connect.side_effect = connect_side_effect instance.echo_interval = 0.001 await instance.connect() assert mock_log_periodic_message.called, "Expected log message to be emitted." def test_disconnect(self, instance_mocked_connection): instance = instance_mocked_connection mock_connection = instance.connection # Create 10 threads to test concurrency. threads = [Thread(target=instance.disconnect) for _ in range(CONCURRENCY_COUNT)] for thread in threads: thread.start() for thread in threads: thread.join() assert mock_connection.close.call_count == 1 # noqa assert instance.connection is None, "Connection was not purged." _expired = "expired" @pytest.mark.asyncio async def test_afetch_retry_authentication_until_successful( self, instance_mocked_connection, monkeypatch, patcher_disconnect ): max_call_count = 2 error = client.sf_connector.errors.ProgrammingError monkeypatch.setattr(error, "__str__", lambda _: self._expired) cursor = instance_mocked_connection.connection.cursor execute_async = cursor.return_value.execute_async execute_async.side_effect = [error] + [ MagicMock() for _ in range(max_call_count - 1) ] await instance_mocked_connection._afetch("sample SQL") assert execute_async.call_count == 2, "Must retry authentication and succeed." @pytest.mark.asyncio async def test_afetch_retry_authentication_attempts_exhausted( self, instance_mocked_connection, monkeypatch, patcher_disconnect ): error = client.sf_connector.errors.ProgrammingError monkeypatch.setattr(error, "__str__", lambda _: self._expired) cursor = instance_mocked_connection.connection.cursor execute_async = cursor.return_value.execute_async execute_async.side_effect = error with pytest.raises(error): # Prevent infinite attempts await instance_mocked_connection._afetch("sample SQL")