Skip to content

Commit e0610e8

Browse files
tests: add unit tests for client routes
Cover ClientRouteEntry/ClientRoutesConfig validation, _RouteStore get/merge operations, _ClientRoutesHandler initialization, ClientRoutesEndPoint resolution with and without route mappings, and SSL check_hostname rejection with client_routes_config.
1 parent 180e0ed commit e0610e8

1 file changed

Lines changed: 211 additions & 0 deletions

File tree

tests/unit/test_client_routes.py

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
# Copyright 2026 ScyllaDB, Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import ssl
16+
import unittest
17+
import uuid
18+
from unittest.mock import Mock, patch
19+
20+
from cassandra.client_routes import (
21+
ClientRouteProxy,
22+
ClientRoutesConfig,
23+
_RouteStore,
24+
_Route,
25+
_ClientRoutesHandler
26+
)
27+
from cassandra.connection import ClientRoutesEndPoint
28+
from cassandra.cluster import Cluster
29+
from cassandra import DriverException
30+
31+
32+
class TestClientRouteProxy(unittest.TestCase):
33+
34+
def test_endpoint_none_connection_id(self):
35+
with self.assertRaises(ValueError):
36+
ClientRouteProxy(None)
37+
38+
39+
class TestClientRoutesConfig(unittest.TestCase):
40+
41+
def test_config_with_proxies(self):
42+
ep1 = ClientRouteProxy(str(uuid.uuid4()), "10.0.0.1")
43+
ep2 = ClientRouteProxy(str(uuid.uuid4()), "10.0.0.2")
44+
config = ClientRoutesConfig([ep1, ep2])
45+
self.assertEqual(len(config.proxies), 2)
46+
47+
def test_config_empty_proxies(self):
48+
with self.assertRaises(ValueError):
49+
ClientRoutesConfig([])
50+
51+
def test_config_invalid_proxy_type(self):
52+
with self.assertRaises(TypeError):
53+
ClientRoutesConfig(["not-a-proxy"])
54+
55+
56+
57+
class TestRouteStore(unittest.TestCase):
58+
59+
def test_get_by_host_id(self):
60+
routes = _RouteStore()
61+
host_id = uuid.uuid4()
62+
route = _Route(
63+
connection_id=str(uuid.uuid4()),
64+
host_id=host_id,
65+
address="example.com",
66+
port=9042,
67+
)
68+
69+
routes.update([route])
70+
71+
retrieved = routes.get_by_host_id(host_id)
72+
self.assertEqual(retrieved.host_id, host_id)
73+
self.assertEqual(retrieved.address, "example.com")
74+
75+
def test_merge_routes(self):
76+
routes = _RouteStore()
77+
host_id1 = uuid.uuid4()
78+
host_id2 = uuid.uuid4()
79+
80+
route1 = _Route(
81+
connection_id=str(uuid.uuid4()), host_id=host_id1,
82+
address="host1.com", port=9042,
83+
)
84+
85+
route2 = _Route(
86+
connection_id=str(uuid.uuid4()), host_id=host_id2,
87+
address="host2.com", port=9042,
88+
)
89+
90+
routes.update([route1])
91+
routes.merge([route2])
92+
93+
self.assertIsNotNone(routes.get_by_host_id(host_id1))
94+
self.assertIsNotNone(routes.get_by_host_id(host_id2))
95+
96+
97+
class TestClientRoutesHandler(unittest.TestCase):
98+
99+
def setUp(self):
100+
self.conn_id = uuid.uuid4()
101+
self.proxy = ClientRouteProxy(str(self.conn_id), "10.0.0.1")
102+
self.config = ClientRoutesConfig([self.proxy])
103+
104+
def test_handler_initialization(self):
105+
handler = _ClientRoutesHandler(self.config, ssl_enabled=False)
106+
self.assertIsNotNone(handler)
107+
self.assertEqual(handler.ssl_enabled, False)
108+
109+
@patch.object(_ClientRoutesHandler, '_query_routes')
110+
def test_initialize(self, mock_query):
111+
host_id = uuid.uuid4()
112+
mock_query.return_value = [
113+
_Route(
114+
connection_id=self.conn_id,
115+
host_id=host_id,
116+
address="node1.example.com",
117+
port=9042,
118+
)
119+
]
120+
121+
handler = _ClientRoutesHandler(self.config)
122+
mock_control_conn = Mock()
123+
124+
handler.initialize(mock_control_conn)
125+
126+
mock_query.assert_called_once()
127+
route = handler._routes.get_by_host_id(host_id)
128+
self.assertIsNotNone(route)
129+
self.assertEqual(route.address, "node1.example.com")
130+
131+
class TestClientRoutesEndPoint(unittest.TestCase):
132+
133+
def setUp(self):
134+
self.conn_id = uuid.uuid4()
135+
self.proxy = ClientRouteProxy(str(self.conn_id), "10.0.0.1")
136+
self.config = ClientRoutesConfig([self.proxy])
137+
self.handler = _ClientRoutesHandler(self.config, ssl_enabled=False)
138+
139+
def test_resolve_falls_back_when_no_mapping(self):
140+
"""resolve() should return original address/port when no route mapping exists."""
141+
host_id = uuid.uuid4()
142+
ep = ClientRoutesEndPoint(
143+
host_id=host_id,
144+
handler=self.handler,
145+
original_address="10.0.0.1",
146+
original_port=9042,
147+
)
148+
self.assertEqual(ep.resolve(), ("10.0.0.1", 9042))
149+
150+
class TestClientRoutesSSLValidation(unittest.TestCase):
151+
152+
def test_check_hostname_with_ssl_context_raises(self):
153+
"""Cluster should reject check_hostname=True with client_routes_config."""
154+
ssl_ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
155+
self.assertTrue(ssl_ctx.check_hostname)
156+
157+
config = ClientRoutesConfig(
158+
proxies=[ClientRouteProxy(str(uuid.uuid4()), "10.0.0.1")]
159+
)
160+
with self.assertRaises(ValueError) as cm:
161+
Cluster(
162+
contact_points=["10.0.0.1"],
163+
ssl_context=ssl_ctx,
164+
client_routes_config=config,
165+
)
166+
self.assertIn("check_hostname", str(cm.exception))
167+
168+
def test_check_hostname_with_ssl_options_raises(self):
169+
"""Cluster should reject check_hostname=True in ssl_options with client_routes_config."""
170+
config = ClientRoutesConfig(
171+
proxies=[ClientRouteProxy(str(uuid.uuid4()), "10.0.0.1")]
172+
)
173+
with self.assertRaises(ValueError) as cm:
174+
Cluster(
175+
contact_points=["10.0.0.1"],
176+
ssl_options={'check_hostname': True},
177+
client_routes_config=config,
178+
)
179+
self.assertIn("check_hostname", str(cm.exception))
180+
181+
def test_disabled_check_hostname_with_client_routes_ok(self):
182+
"""Cluster should allow check_hostname=False with client_routes_config."""
183+
ssl_ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
184+
ssl_ctx.check_hostname = False
185+
186+
config = ClientRoutesConfig(
187+
proxies=[ClientRouteProxy(str(uuid.uuid4()), "10.0.0.1")]
188+
)
189+
# Should not raise
190+
cluster = Cluster(
191+
contact_points=["10.0.0.1"],
192+
ssl_context=ssl_ctx,
193+
client_routes_config=config,
194+
)
195+
cluster.shutdown()
196+
197+
def test_no_ssl_with_client_routes_ok(self):
198+
"""Cluster should allow client_routes_config without SSL."""
199+
config = ClientRoutesConfig(
200+
proxies=[ClientRouteProxy(str(uuid.uuid4()), "10.0.0.1")]
201+
)
202+
# Should not raise
203+
cluster = Cluster(
204+
contact_points=["10.0.0.1"],
205+
client_routes_config=config,
206+
)
207+
cluster.shutdown()
208+
209+
210+
if __name__ == '__main__':
211+
unittest.main()

0 commit comments

Comments
 (0)