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)