from unittest.mock import patch, MagicMock from ja4_common.clickhouse import ClickHouseClient, get_client import ja4_common.clickhouse as ch_module def test_get_client_singleton(): ch_module._client = None with patch("ja4_common.clickhouse.clickhouse_connect.get_client") as mock_gc: mock_client = MagicMock() mock_client.ping.return_value = True mock_gc.return_value = mock_client c1 = get_client() c2 = get_client() assert c1 is c2 def test_client_reconnects_on_ping_fail(): client = ClickHouseClient() with patch("ja4_common.clickhouse.clickhouse_connect.get_client") as mock_gc: mock_inner = MagicMock() mock_inner.ping.side_effect = Exception("connection lost") mock_gc.return_value = mock_inner client._client = mock_inner # simulate stale connection client.connect() # should reconnect assert mock_gc.call_count >= 1 def test_get_client_returns_same_instance_on_second_call(): """get_client() is a singleton: returns the same object on repeated calls.""" ch_module._client = None with patch("ja4_common.clickhouse.clickhouse_connect.get_client") as mock_gc: mock_inner = MagicMock() mock_inner.ping.return_value = True mock_gc.return_value = mock_inner c1 = get_client() c2 = get_client() assert c1 is c2 # connect() should have been called once for c1; c2 reuses the same instance assert mock_gc.call_count == 1 def test_client_query_delegates_to_inner(): """ClickHouseClient.query() delegates to the underlying client.""" client = ClickHouseClient() with patch("ja4_common.clickhouse.clickhouse_connect.get_client") as mock_gc: mock_inner = MagicMock() mock_inner.ping.return_value = True mock_inner.query.return_value = "result" mock_gc.return_value = mock_inner result = client.query("SELECT 1") assert result == "result" mock_inner.query.assert_called_once_with("SELECT 1", None) def test_client_query_with_params(): """ClickHouseClient.query() passes params to the inner client.""" client = ClickHouseClient() with patch("ja4_common.clickhouse.clickhouse_connect.get_client") as mock_gc: mock_inner = MagicMock() mock_inner.ping.return_value = True mock_gc.return_value = mock_inner client.query("SELECT %(val)s", {"val": 42}) mock_inner.query.assert_called_once_with("SELECT %(val)s", {"val": 42}) def test_client_close_sets_client_to_none(): """ClickHouseClient.close() clears the internal client reference.""" client = ClickHouseClient() with patch("ja4_common.clickhouse.clickhouse_connect.get_client") as mock_gc: mock_inner = MagicMock() mock_inner.ping.return_value = True mock_gc.return_value = mock_inner client.connect() # establish connection assert client._client is not None client.close() assert client._client is None def test_client_close_when_already_none(): """ClickHouseClient.close() is safe to call when no connection exists.""" client = ClickHouseClient() client._client = None # ensure no connection client.close() # should not raise def test_ping_returns_false_on_exception(): """_ping() returns False when ping raises.""" client = ClickHouseClient() mock_inner = MagicMock() mock_inner.ping.side_effect = Exception("conn reset") client._client = mock_inner assert client._ping() is False def test_ping_returns_true_on_success(): """_ping() returns True when ping succeeds.""" client = ClickHouseClient() mock_inner = MagicMock() mock_inner.ping.return_value = True client._client = mock_inner assert client._ping() is True def test_ping_returns_false_when_no_client(): """_ping() returns False when _client is None.""" client = ClickHouseClient() client._client = None assert client._ping() is False