@@ -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