Netdata.cloud bot for Zulip
1"""Tests for the certificate manager module.""" 2 3import tempfile 4from pathlib import Path 5from unittest.mock import Mock, patch, MagicMock 6 7import pytest 8 9from netdata_zulip_bot.cert_manager import CertificateManager 10 11 12class TestCertificateManager: 13 """Test certificate manager functionality.""" 14 15 @pytest.fixture 16 def temp_cert_dir(self): 17 """Create a temporary directory for certificates.""" 18 with tempfile.TemporaryDirectory() as tmpdir: 19 yield Path(tmpdir) 20 21 @pytest.fixture 22 def cert_manager(self, temp_cert_dir): 23 """Create a certificate manager instance.""" 24 return CertificateManager( 25 domain="test.example.com", 26 email="test@example.com", 27 cert_dir=temp_cert_dir, 28 staging=True, # Always use staging for tests 29 port=8080 30 ) 31 32 def test_initialization(self, cert_manager, temp_cert_dir): 33 """Test certificate manager initialization.""" 34 assert cert_manager.domain == "test.example.com" 35 assert cert_manager.email == "test@example.com" 36 assert cert_manager.cert_dir == temp_cert_dir 37 assert cert_manager.staging is True 38 assert cert_manager.challenge_port == 8080 39 40 # Check that paths are created correctly 41 assert cert_manager.account_key_path == temp_cert_dir / "account_key.pem" 42 assert cert_manager.cert_path == temp_cert_dir / "test.example.com_cert.pem" 43 assert cert_manager.key_path == temp_cert_dir / "test.example.com_key.pem" 44 assert cert_manager.fullchain_path == temp_cert_dir / "test.example.com_fullchain.pem" 45 46 def test_cert_dir_creation(self, temp_cert_dir): 47 """Test that certificate directory is created if it doesn't exist.""" 48 new_dir = temp_cert_dir / "nested" / "certs" 49 cert_manager = CertificateManager( 50 domain="test.example.com", 51 email="test@example.com", 52 cert_dir=new_dir, 53 staging=True 54 ) 55 assert new_dir.exists() 56 assert new_dir.is_dir() 57 58 @patch('netdata_zulip_bot.cert_manager.x509') 59 def test_needs_renewal_no_cert(self, mock_x509, cert_manager): 60 """Test that renewal is needed when certificate doesn't exist.""" 61 assert cert_manager.needs_renewal() is True 62 63 @patch('netdata_zulip_bot.cert_manager.datetime') 64 @patch('netdata_zulip_bot.cert_manager.x509') 65 def test_needs_renewal_expired(self, mock_x509, mock_datetime, cert_manager): 66 """Test that renewal is needed when certificate is expiring soon.""" 67 from datetime import datetime, timezone, timedelta 68 69 # Create a mock certificate file 70 cert_manager.cert_path.touch() 71 72 # Mock certificate with 20 days remaining 73 mock_cert = Mock() 74 now = datetime(2024, 1, 1, tzinfo=timezone.utc) 75 mock_cert.not_valid_after_utc = now + timedelta(days=20) 76 mock_x509.load_pem_x509_certificate.return_value = mock_cert 77 mock_datetime.now.return_value = now 78 79 assert cert_manager.needs_renewal() is True 80 81 @patch('netdata_zulip_bot.cert_manager.datetime') 82 @patch('netdata_zulip_bot.cert_manager.x509') 83 def test_needs_renewal_valid(self, mock_x509, mock_datetime, cert_manager): 84 """Test that renewal is not needed when certificate is still valid.""" 85 from datetime import datetime, timezone, timedelta 86 87 # Create a mock certificate file 88 cert_manager.cert_path.touch() 89 90 # Mock certificate with 60 days remaining 91 mock_cert = Mock() 92 now = datetime(2024, 1, 1, tzinfo=timezone.utc) 93 mock_cert.not_valid_after_utc = now + timedelta(days=60) 94 mock_x509.load_pem_x509_certificate.return_value = mock_cert 95 mock_datetime.now.return_value = now 96 97 assert cert_manager.needs_renewal() is False 98 99 def test_generate_private_key(self, cert_manager): 100 """Test private key generation.""" 101 key = cert_manager._generate_private_key() 102 assert key is not None 103 assert key.key_size == 2048 104 105 @patch('netdata_zulip_bot.cert_manager.threading.Thread') 106 def test_challenge_server_start(self, mock_thread, cert_manager): 107 """Test that challenge server starts correctly.""" 108 cert_manager._start_challenge_server() 109 110 # Verify thread was created and started 111 mock_thread.assert_called_once() 112 mock_thread.return_value.start.assert_called_once() 113 114 def test_challenge_tokens_storage(self, cert_manager): 115 """Test that challenge tokens are stored correctly.""" 116 cert_manager.challenge_tokens["test_token"] = "test_response" 117 assert cert_manager.challenge_tokens["test_token"] == "test_response" 118 119 @patch('netdata_zulip_bot.cert_manager.client.ClientV2') 120 @patch('netdata_zulip_bot.cert_manager.client.ClientNetwork') 121 def test_obtain_certificate_mock(self, mock_network, mock_client, cert_manager): 122 """Test certificate obtaining with mocked ACME client.""" 123 # This is a simplified test that mocks the ACME interaction 124 # In production, this would interact with Let's Encrypt staging server 125 126 # Mock that certificate doesn't need renewal 127 with patch.object(cert_manager, 'needs_renewal', return_value=False): 128 paths = cert_manager.obtain_certificate() 129 assert paths == (cert_manager.cert_path, cert_manager.key_path, cert_manager.fullchain_path)