|
11 | 11 | """ |
12 | 12 |
|
13 | 13 | import json |
| 14 | +import ssl |
14 | 15 |
|
15 | | -from unittest.mock import MagicMock |
| 16 | +from unittest.mock import MagicMock, patch |
16 | 17 |
|
17 | 18 | import pytest |
18 | 19 |
|
@@ -531,3 +532,204 @@ def release_conn(self): |
531 | 532 | # Exception is logged, we yield nothing |
532 | 533 | assert results == [] |
533 | 534 | mock_pool_manager.request.assert_called_once() |
| 535 | + |
| 536 | + |
| 537 | +# Tests for SSL Context Reuse (fix for OpenSSL 3.0+ performance issues) |
| 538 | +@patch('ssl.create_default_context') |
| 539 | +@patch('urllib3.PoolManager') |
| 540 | +def test_ssl_context_created_with_ca_cert(mock_pool_manager, mock_create_context): |
| 541 | + """Test that SSL context is created with CA certificate file.""" |
| 542 | + mock_ssl_context = MagicMock() |
| 543 | + mock_create_context.return_value = mock_ssl_context |
| 544 | + |
| 545 | + mock_config = MagicMock() |
| 546 | + mock_config.ssl_ca_cert = "/path/to/ca.pem" |
| 547 | + mock_config.cert_file = None |
| 548 | + mock_config.key_file = None |
| 549 | + mock_config.verify_ssl = True |
| 550 | + mock_config.connection_pool_maxsize = 4 |
| 551 | + mock_config.timeout_millisec = 5000 |
| 552 | + mock_config.proxy = None |
| 553 | + |
| 554 | + RESTClientObject(configuration=mock_config) |
| 555 | + |
| 556 | + # Verify SSL context was created with CA file |
| 557 | + mock_create_context.assert_called_once_with(cafile="/path/to/ca.pem") |
| 558 | + |
| 559 | + # Verify SSL context was passed to PoolManager |
| 560 | + mock_pool_manager.assert_called_once() |
| 561 | + call_kwargs = mock_pool_manager.call_args[1] |
| 562 | + assert call_kwargs['ssl_context'] == mock_ssl_context |
| 563 | + |
| 564 | + |
| 565 | +@patch('ssl.create_default_context') |
| 566 | +@patch('urllib3.PoolManager') |
| 567 | +def test_ssl_context_loads_client_certificate(mock_pool_manager, mock_create_context): |
| 568 | + """Test that SSL context loads client certificate and key when provided.""" |
| 569 | + mock_ssl_context = MagicMock() |
| 570 | + mock_create_context.return_value = mock_ssl_context |
| 571 | + |
| 572 | + mock_config = MagicMock() |
| 573 | + mock_config.ssl_ca_cert = None |
| 574 | + mock_config.cert_file = "/path/to/client.pem" |
| 575 | + mock_config.key_file = "/path/to/client.key" |
| 576 | + mock_config.verify_ssl = True |
| 577 | + mock_config.connection_pool_maxsize = 4 |
| 578 | + mock_config.timeout_millisec = 5000 |
| 579 | + mock_config.proxy = None |
| 580 | + |
| 581 | + RESTClientObject(configuration=mock_config) |
| 582 | + |
| 583 | + # Verify SSL context was created |
| 584 | + mock_create_context.assert_called_once_with(cafile=None) |
| 585 | + |
| 586 | + # Verify client certificate was loaded |
| 587 | + mock_ssl_context.load_cert_chain.assert_called_once_with( |
| 588 | + "/path/to/client.pem", keyfile="/path/to/client.key" |
| 589 | + ) |
| 590 | + |
| 591 | + # Verify SSL context was passed to PoolManager |
| 592 | + mock_pool_manager.assert_called_once() |
| 593 | + call_kwargs = mock_pool_manager.call_args[1] |
| 594 | + assert call_kwargs['ssl_context'] == mock_ssl_context |
| 595 | + |
| 596 | + |
| 597 | +@patch('ssl.create_default_context') |
| 598 | +@patch('urllib3.PoolManager') |
| 599 | +def test_ssl_context_disables_verification_when_verify_ssl_false(mock_pool_manager, mock_create_context): |
| 600 | + """Test that SSL context disables verification when verify_ssl=False.""" |
| 601 | + mock_ssl_context = MagicMock() |
| 602 | + mock_create_context.return_value = mock_ssl_context |
| 603 | + |
| 604 | + mock_config = MagicMock() |
| 605 | + mock_config.ssl_ca_cert = None |
| 606 | + mock_config.cert_file = None |
| 607 | + mock_config.key_file = None |
| 608 | + mock_config.verify_ssl = False |
| 609 | + mock_config.connection_pool_maxsize = 4 |
| 610 | + mock_config.timeout_millisec = 5000 |
| 611 | + mock_config.proxy = None |
| 612 | + |
| 613 | + RESTClientObject(configuration=mock_config) |
| 614 | + |
| 615 | + # Verify SSL context was created |
| 616 | + mock_create_context.assert_called_once_with(cafile=None) |
| 617 | + |
| 618 | + # Verify SSL verification was disabled |
| 619 | + assert mock_ssl_context.check_hostname is False |
| 620 | + assert mock_ssl_context.verify_mode == ssl.CERT_NONE |
| 621 | + |
| 622 | + # Verify SSL context was passed to PoolManager |
| 623 | + mock_pool_manager.assert_called_once() |
| 624 | + call_kwargs = mock_pool_manager.call_args[1] |
| 625 | + assert call_kwargs['ssl_context'] == mock_ssl_context |
| 626 | + |
| 627 | + |
| 628 | +@patch('ssl.create_default_context') |
| 629 | +@patch('urllib3.ProxyManager') |
| 630 | +def test_ssl_context_used_with_proxy_manager(mock_proxy_manager, mock_create_context): |
| 631 | + """Test that SSL context is passed to ProxyManager when proxy is configured.""" |
| 632 | + mock_ssl_context = MagicMock() |
| 633 | + mock_create_context.return_value = mock_ssl_context |
| 634 | + |
| 635 | + mock_config = MagicMock() |
| 636 | + mock_config.ssl_ca_cert = "/path/to/ca.pem" |
| 637 | + mock_config.cert_file = "/path/to/client.pem" |
| 638 | + mock_config.key_file = "/path/to/client.key" |
| 639 | + mock_config.verify_ssl = True |
| 640 | + mock_config.connection_pool_maxsize = 4 |
| 641 | + mock_config.timeout_millisec = 5000 |
| 642 | + mock_config.proxy = "http://proxy:8080" |
| 643 | + mock_config.proxy_headers = {"Proxy-Auth": "token"} |
| 644 | + |
| 645 | + RESTClientObject(configuration=mock_config) |
| 646 | + |
| 647 | + # Verify SSL context was created with CA file |
| 648 | + mock_create_context.assert_called_once_with(cafile="/path/to/ca.pem") |
| 649 | + |
| 650 | + # Verify client certificate was loaded |
| 651 | + mock_ssl_context.load_cert_chain.assert_called_once_with( |
| 652 | + "/path/to/client.pem", keyfile="/path/to/client.key" |
| 653 | + ) |
| 654 | + |
| 655 | + # Verify SSL context was passed to ProxyManager |
| 656 | + mock_proxy_manager.assert_called_once() |
| 657 | + call_kwargs = mock_proxy_manager.call_args[1] |
| 658 | + assert call_kwargs['ssl_context'] == mock_ssl_context |
| 659 | + assert call_kwargs['proxy_url'] == "http://proxy:8080" |
| 660 | + assert call_kwargs['proxy_headers'] == {"Proxy-Auth": "token"} |
| 661 | + |
| 662 | + |
| 663 | +@patch('ssl.create_default_context') |
| 664 | +@patch('urllib3.PoolManager') |
| 665 | +def test_ssl_context_reuse_performance_optimization(mock_pool_manager, mock_create_context): |
| 666 | + """Test that SSL context creation is called only once per client instance.""" |
| 667 | + mock_ssl_context = MagicMock() |
| 668 | + mock_create_context.return_value = mock_ssl_context |
| 669 | + |
| 670 | + mock_config = MagicMock() |
| 671 | + mock_config.ssl_ca_cert = "/path/to/ca.pem" |
| 672 | + mock_config.cert_file = None |
| 673 | + mock_config.key_file = None |
| 674 | + mock_config.verify_ssl = True |
| 675 | + mock_config.connection_pool_maxsize = 4 |
| 676 | + mock_config.timeout_millisec = 5000 |
| 677 | + mock_config.proxy = None |
| 678 | + |
| 679 | + # Create client instance |
| 680 | + client = RESTClientObject(configuration=mock_config) |
| 681 | + |
| 682 | + # Verify SSL context was created exactly once |
| 683 | + mock_create_context.assert_called_once_with(cafile="/path/to/ca.pem") |
| 684 | + |
| 685 | + # Verify the same SSL context instance is reused |
| 686 | + mock_pool_manager.assert_called_once() |
| 687 | + call_kwargs = mock_pool_manager.call_args[1] |
| 688 | + assert call_kwargs['ssl_context'] is mock_ssl_context |
| 689 | + |
| 690 | + # Verify context was not created again during subsequent operations |
| 691 | + mock_create_context.reset_mock() |
| 692 | + |
| 693 | + # Build a request (this should not trigger SSL context creation) |
| 694 | + client.build_request("GET", "https://example.com") |
| 695 | + |
| 696 | + # SSL context should not be created again |
| 697 | + mock_create_context.assert_not_called() |
| 698 | + |
| 699 | + |
| 700 | +@patch('ssl.create_default_context') |
| 701 | +@patch('urllib3.PoolManager') |
| 702 | +def test_ssl_context_with_all_ssl_options(mock_pool_manager, mock_create_context): |
| 703 | + """Test SSL context creation with all SSL configuration options set.""" |
| 704 | + mock_ssl_context = MagicMock() |
| 705 | + mock_create_context.return_value = mock_ssl_context |
| 706 | + |
| 707 | + mock_config = MagicMock() |
| 708 | + mock_config.ssl_ca_cert = "/path/to/ca.pem" |
| 709 | + mock_config.cert_file = "/path/to/client.pem" |
| 710 | + mock_config.key_file = "/path/to/client.key" |
| 711 | + mock_config.verify_ssl = True |
| 712 | + mock_config.connection_pool_maxsize = 8 |
| 713 | + mock_config.timeout_millisec = 10000 |
| 714 | + mock_config.proxy = None |
| 715 | + |
| 716 | + RESTClientObject(configuration=mock_config) |
| 717 | + |
| 718 | + # Verify SSL context was created with CA file |
| 719 | + mock_create_context.assert_called_once_with(cafile="/path/to/ca.pem") |
| 720 | + |
| 721 | + # Verify client certificate was loaded |
| 722 | + mock_ssl_context.load_cert_chain.assert_called_once_with( |
| 723 | + "/path/to/client.pem", keyfile="/path/to/client.key" |
| 724 | + ) |
| 725 | + |
| 726 | + # Verify SSL verification settings were NOT modified (verify_ssl=True) |
| 727 | + # check_hostname and verify_mode should remain at their default secure values |
| 728 | + assert not hasattr(mock_ssl_context, 'check_hostname') or mock_ssl_context.check_hostname |
| 729 | + assert not hasattr(mock_ssl_context, 'verify_mode') or mock_ssl_context.verify_mode != ssl.CERT_NONE |
| 730 | + |
| 731 | + # Verify SSL context was passed to PoolManager |
| 732 | + mock_pool_manager.assert_called_once() |
| 733 | + call_kwargs = mock_pool_manager.call_args[1] |
| 734 | + assert call_kwargs['ssl_context'] == mock_ssl_context |
| 735 | + assert call_kwargs['maxsize'] == 8 |
0 commit comments