Skip to content

Commit 36a71f9

Browse files
committed
test: add unit tests for TwistedConnection TLS session caching
Add tests for TLS session caching in the Twisted reactor: - Cached session is applied in clientConnectionForTLS() - Session is stored in info_callback() after handshake - Session reuse is detected and logged - _SSLCreator properly receives and uses tls_session_cache
1 parent a36aa63 commit 36a71f9

1 file changed

Lines changed: 236 additions & 0 deletions

File tree

tests/unit/io/test_twistedreactor.py

Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,3 +188,239 @@ def test_push(self, mock_connectTCP):
188188
self.obj_ut.push('123 pickup')
189189
self.mock_reactor_cft.assert_called_with(
190190
transport_mock.write, '123 pickup')
191+
192+
193+
try:
194+
from OpenSSL import SSL as PyOpenSSL
195+
_HAS_PYOPENSSL = True
196+
except ImportError:
197+
_HAS_PYOPENSSL = False
198+
199+
200+
@unittest.skipIf(twistedreactor is None, "Twisted libraries not available")
201+
@unittest.skipIf(not _HAS_PYOPENSSL, "PyOpenSSL not available")
202+
class TestSSLCreatorTLSSessionCache(unittest.TestCase):
203+
"""Test TLS session caching for _SSLCreator with PyOpenSSL."""
204+
205+
def setUp(self):
206+
twistedreactor.TwistedConnection.initialize_reactor()
207+
208+
def tearDown(self):
209+
loop = twistedreactor.TwistedConnection._loop
210+
if loop and not loop._reactor_stopped():
211+
loop._cleanup()
212+
213+
def test_client_connection_applies_cached_session(self):
214+
"""Test that clientConnectionForTLS applies cached TLS session."""
215+
mock_cache = Mock()
216+
mock_session = Mock()
217+
mock_cache.get_session.return_value = mock_session
218+
219+
mock_ssl_context = Mock()
220+
mock_ssl_connection = Mock()
221+
222+
endpoint = DefaultEndPoint('127.0.0.1', 9042)
223+
224+
with patch('cassandra.io.twistedreactor.SSL.Connection', return_value=mock_ssl_connection):
225+
creator = twistedreactor._SSLCreator(
226+
endpoint=endpoint,
227+
ssl_context=mock_ssl_context,
228+
ssl_options={},
229+
check_hostname=False,
230+
timeout=5,
231+
tls_session_cache=mock_cache
232+
)
233+
234+
mock_tls_protocol = Mock()
235+
creator.clientConnectionForTLS(mock_tls_protocol)
236+
237+
# Verify get_session was called with endpoint
238+
mock_cache.get_session.assert_called_once_with(endpoint)
239+
240+
# Verify set_session was called on the SSL connection
241+
mock_ssl_connection.set_session.assert_called_once_with(mock_session)
242+
243+
def test_client_connection_no_session_when_cache_empty(self):
244+
"""Test that clientConnectionForTLS handles empty cache."""
245+
mock_cache = Mock()
246+
mock_cache.get_session.return_value = None # No cached session
247+
248+
mock_ssl_context = Mock()
249+
mock_ssl_connection = Mock()
250+
251+
endpoint = DefaultEndPoint('127.0.0.1', 9042)
252+
253+
with patch('cassandra.io.twistedreactor.SSL.Connection', return_value=mock_ssl_connection):
254+
creator = twistedreactor._SSLCreator(
255+
endpoint=endpoint,
256+
ssl_context=mock_ssl_context,
257+
ssl_options={},
258+
check_hostname=False,
259+
timeout=5,
260+
tls_session_cache=mock_cache
261+
)
262+
263+
mock_tls_protocol = Mock()
264+
creator.clientConnectionForTLS(mock_tls_protocol)
265+
266+
# Verify get_session was called
267+
mock_cache.get_session.assert_called_once_with(endpoint)
268+
269+
# Verify set_session was NOT called on SSL connection
270+
mock_ssl_connection.set_session.assert_not_called()
271+
272+
def test_client_connection_no_cache_configured(self):
273+
"""Test that clientConnectionForTLS works without a cache."""
274+
mock_ssl_context = Mock()
275+
mock_ssl_connection = Mock()
276+
277+
endpoint = DefaultEndPoint('127.0.0.1', 9042)
278+
279+
with patch('cassandra.io.twistedreactor.SSL.Connection', return_value=mock_ssl_connection):
280+
creator = twistedreactor._SSLCreator(
281+
endpoint=endpoint,
282+
ssl_context=mock_ssl_context,
283+
ssl_options={},
284+
check_hostname=False,
285+
timeout=5,
286+
tls_session_cache=None # No cache
287+
)
288+
289+
mock_tls_protocol = Mock()
290+
result = creator.clientConnectionForTLS(mock_tls_protocol)
291+
292+
# Should return the connection without errors
293+
self.assertEqual(result, mock_ssl_connection)
294+
295+
# Verify set_session was NOT called
296+
mock_ssl_connection.set_session.assert_not_called()
297+
298+
def test_info_callback_stores_session_after_handshake(self):
299+
"""Test that info_callback stores session after handshake."""
300+
mock_cache = Mock()
301+
mock_session = Mock()
302+
303+
mock_ssl_context = Mock()
304+
mock_ssl_connection = Mock()
305+
mock_ssl_connection.get_session.return_value = mock_session
306+
mock_ssl_connection.session_reused.return_value = False
307+
mock_ssl_connection.get_peer_certificate.return_value.get_subject.return_value.commonName = '127.0.0.1'
308+
309+
endpoint = DefaultEndPoint('127.0.0.1', 9042)
310+
311+
with patch('cassandra.io.twistedreactor.SSL.Connection', return_value=mock_ssl_connection):
312+
creator = twistedreactor._SSLCreator(
313+
endpoint=endpoint,
314+
ssl_context=mock_ssl_context,
315+
ssl_options={},
316+
check_hostname=False,
317+
timeout=5,
318+
tls_session_cache=mock_cache
319+
)
320+
321+
# Simulate handshake completion
322+
creator.info_callback(mock_ssl_connection, PyOpenSSL.SSL_CB_HANDSHAKE_DONE, 0)
323+
324+
# Verify session was retrieved and stored
325+
mock_ssl_connection.get_session.assert_called_once()
326+
mock_cache.set_session.assert_called_once_with(endpoint, mock_session)
327+
328+
def test_info_callback_logs_session_reuse(self):
329+
"""Test that info_callback logs when session is reused."""
330+
mock_cache = Mock()
331+
mock_session = Mock()
332+
333+
mock_ssl_context = Mock()
334+
mock_ssl_connection = Mock()
335+
mock_ssl_connection.get_session.return_value = mock_session
336+
mock_ssl_connection.session_reused.return_value = True # Session was reused
337+
mock_ssl_connection.get_peer_certificate.return_value.get_subject.return_value.commonName = '127.0.0.1'
338+
339+
endpoint = DefaultEndPoint('127.0.0.1', 9042)
340+
341+
with patch('cassandra.io.twistedreactor.SSL.Connection', return_value=mock_ssl_connection):
342+
creator = twistedreactor._SSLCreator(
343+
endpoint=endpoint,
344+
ssl_context=mock_ssl_context,
345+
ssl_options={},
346+
check_hostname=False,
347+
timeout=5,
348+
tls_session_cache=mock_cache
349+
)
350+
351+
with patch('cassandra.io.twistedreactor.log') as mock_log:
352+
creator.info_callback(mock_ssl_connection, PyOpenSSL.SSL_CB_HANDSHAKE_DONE, 0)
353+
354+
# Verify session_reused was checked
355+
mock_ssl_connection.session_reused.assert_called_once()
356+
357+
# Verify debug log was called for session reuse
358+
mock_log.debug.assert_called()
359+
360+
def test_info_callback_no_session_store_when_no_cache(self):
361+
"""Test that info_callback doesn't store session when no cache configured."""
362+
mock_ssl_context = Mock()
363+
mock_ssl_connection = Mock()
364+
mock_ssl_connection.get_peer_certificate.return_value.get_subject.return_value.commonName = '127.0.0.1'
365+
366+
endpoint = DefaultEndPoint('127.0.0.1', 9042)
367+
368+
with patch('cassandra.io.twistedreactor.SSL.Connection', return_value=mock_ssl_connection):
369+
creator = twistedreactor._SSLCreator(
370+
endpoint=endpoint,
371+
ssl_context=mock_ssl_context,
372+
ssl_options={},
373+
check_hostname=False,
374+
timeout=5,
375+
tls_session_cache=None # No cache
376+
)
377+
378+
creator.info_callback(mock_ssl_connection, PyOpenSSL.SSL_CB_HANDSHAKE_DONE, 0)
379+
380+
# Verify get_session was NOT called
381+
mock_ssl_connection.get_session.assert_not_called()
382+
383+
384+
@unittest.skipIf(twistedreactor is None, "Twisted libraries not available")
385+
@unittest.skipIf(not _HAS_PYOPENSSL, "PyOpenSSL not available")
386+
class TestTwistedConnectionTLSSessionCache(unittest.TestCase):
387+
"""Test TLS session caching integration in TwistedConnection."""
388+
389+
def setUp(self):
390+
if twistedreactor.TwistedConnection._loop:
391+
twistedreactor.TwistedConnection._loop._cleanup()
392+
twistedreactor.TwistedConnection.initialize_reactor()
393+
self.reactor_cft_patcher = patch('twisted.internet.reactor.callFromThread')
394+
self.reactor_run_patcher = patch('twisted.internet.reactor.run')
395+
self.mock_reactor_cft = self.reactor_cft_patcher.start()
396+
self.mock_reactor_run = self.reactor_run_patcher.start()
397+
398+
def tearDown(self):
399+
self.reactor_cft_patcher.stop()
400+
self.reactor_run_patcher.stop()
401+
402+
def test_add_connection_passes_tls_session_cache(self):
403+
"""Test that add_connection passes tls_session_cache to _SSLCreator."""
404+
mock_cache = Mock()
405+
mock_ssl_context = Mock()
406+
407+
endpoint = DefaultEndPoint('127.0.0.1', 9042)
408+
409+
conn = twistedreactor.TwistedConnection(
410+
endpoint,
411+
cql_version='3.0.1',
412+
connect_timeout=5
413+
)
414+
conn.ssl_context = mock_ssl_context
415+
conn.ssl_options = {}
416+
conn.tls_session_cache = mock_cache
417+
418+
with patch('cassandra.io.twistedreactor._SSLCreator') as mock_creator_class:
419+
with patch('cassandra.io.twistedreactor.SSL4ClientEndpoint'):
420+
with patch('cassandra.io.twistedreactor.connectProtocol'):
421+
conn.add_connection()
422+
423+
# Verify _SSLCreator was called with tls_session_cache
424+
mock_creator_class.assert_called_once()
425+
call_kwargs = mock_creator_class.call_args
426+
self.assertEqual(call_kwargs.kwargs.get('tls_session_cache'), mock_cache)

0 commit comments

Comments
 (0)