mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2025-11-03 03:38:15 +00:00
Enhance CertUtils to return the newest matching certificate and add comprehensive unit tests
- Added run_openssl_dates() to extract notBefore/notAfter timestamps. - Modified mapping logic to store multiple cert entries per SAN with metadata. - find_cert_for_domain() now selects the newest certificate based on notBefore and mtime. - Exact SAN matches take precedence over wildcard matches. - Added new unit tests (test_cert_utils_newest.py) verifying freshness logic, fallback handling, and wildcard behavior. Reference: https://chatgpt.com/share/68ef4b4c-41d4-800f-9e50-5da4b6be1105
This commit is contained in:
172
tests/unit/module_utils/test_cert_utils_newest.py
Normal file
172
tests/unit/module_utils/test_cert_utils_newest.py
Normal file
@@ -0,0 +1,172 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
|
||||
# Add the project root/module_utils to the import path
|
||||
CURRENT_DIR = os.path.dirname(__file__)
|
||||
PROJECT_ROOT = os.path.abspath(os.path.join(CURRENT_DIR, "../../.."))
|
||||
sys.path.insert(0, PROJECT_ROOT)
|
||||
|
||||
from module_utils.cert_utils import CertUtils
|
||||
|
||||
|
||||
def _san_block(*entries):
|
||||
"""
|
||||
Helper: builds a minimal OpenSSL text snippet that contains SAN entries.
|
||||
Example: _san_block('example.com', '*.example.com')
|
||||
"""
|
||||
sans = ", ".join(f"DNS:{e}" for e in entries)
|
||||
return f"""
|
||||
Certificate:
|
||||
Data:
|
||||
Version: 3 (0x2)
|
||||
...
|
||||
X509v3 extensions:
|
||||
X509v3 Subject Alternative Name:
|
||||
{sans}
|
||||
"""
|
||||
|
||||
|
||||
class TestCertUtilsFindNewest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
# Reset internal caches before each test
|
||||
CertUtils._domain_cert_mapping = None
|
||||
CertUtils._cert_snapshot = None
|
||||
|
||||
def _mock_stat_map(self, mtime_map, size_map=None):
|
||||
size_map = size_map or {}
|
||||
def _stat_side_effect(path):
|
||||
return SimpleNamespace(
|
||||
st_mtime=mtime_map.get(path, 0.0),
|
||||
st_size=size_map.get(path, 1234),
|
||||
)
|
||||
return _stat_side_effect
|
||||
|
||||
def test_prefers_newest_by_not_before(self):
|
||||
"""
|
||||
Two certs with the same SAN 'www.example.com':
|
||||
- a/cert.pem: older notBefore
|
||||
- b/cert.pem: newer notBefore -> should be selected
|
||||
"""
|
||||
files = [
|
||||
"/etc/letsencrypt/live/a/cert.pem",
|
||||
"/etc/letsencrypt/live/b/cert.pem",
|
||||
]
|
||||
san_text = _san_block("www.example.com")
|
||||
|
||||
with patch.object(CertUtils, "list_cert_files", return_value=files), \
|
||||
patch.object(CertUtils, "run_openssl", return_value=san_text), \
|
||||
patch.object(CertUtils, "run_openssl_dates") as mock_dates, \
|
||||
patch("os.stat", side_effect=self._mock_stat_map({
|
||||
files[0]: 1000,
|
||||
files[1]: 1001,
|
||||
})):
|
||||
|
||||
mock_dates.side_effect = [(10, 100000), (20, 100000)] # older/newer
|
||||
|
||||
folder = CertUtils.find_cert_for_domain("www.example.com", "/etc/letsencrypt/live", debug=False)
|
||||
self.assertEqual(folder, "b", "Should return the folder with the newest notBefore date.")
|
||||
|
||||
def test_fallback_to_mtime_when_not_before_missing(self):
|
||||
"""
|
||||
When not_before is missing, mtime should be used as a fallback.
|
||||
"""
|
||||
files = [
|
||||
"/etc/letsencrypt/live/a/cert.pem",
|
||||
"/etc/letsencrypt/live/b/cert.pem",
|
||||
]
|
||||
san_text = _san_block("www.example.com")
|
||||
|
||||
with patch.object(CertUtils, "list_cert_files", return_value=files), \
|
||||
patch.object(CertUtils, "run_openssl", return_value=san_text), \
|
||||
patch.object(CertUtils, "run_openssl_dates", return_value=(None, None)), \
|
||||
patch("os.stat", side_effect=self._mock_stat_map({
|
||||
files[0]: 1000,
|
||||
files[1]: 2000,
|
||||
})):
|
||||
|
||||
folder = CertUtils.find_cert_for_domain("www.example.com", "/etc/letsencrypt/live", debug=False)
|
||||
self.assertEqual(folder, "b", "Should fall back to mtime and select the newest file.")
|
||||
|
||||
def test_exact_beats_wildcard_even_if_wildcard_newer(self):
|
||||
"""
|
||||
Exact matches must take precedence over wildcard matches,
|
||||
even if the wildcard certificate is newer.
|
||||
"""
|
||||
files = [
|
||||
"/etc/letsencrypt/live/exact/cert.pem",
|
||||
"/etc/letsencrypt/live/wild/cert.pem",
|
||||
]
|
||||
text_exact = _san_block("api.example.com")
|
||||
text_wild = _san_block("*.example.com")
|
||||
|
||||
with patch.object(CertUtils, "list_cert_files", return_value=files), \
|
||||
patch.object(CertUtils, "run_openssl") as mock_text, \
|
||||
patch.object(CertUtils, "run_openssl_dates") as mock_dates, \
|
||||
patch("os.stat", side_effect=self._mock_stat_map({
|
||||
files[0]: 1000, # exact is older
|
||||
files[1]: 5000, # wildcard is much newer
|
||||
})):
|
||||
|
||||
mock_text.side_effect = [text_exact, text_wild]
|
||||
mock_dates.side_effect = [(10, 100000), (99, 100000)]
|
||||
|
||||
folder = CertUtils.find_cert_for_domain("api.example.com", "/etc/letsencrypt/live", debug=False)
|
||||
self.assertEqual(
|
||||
folder, "exact",
|
||||
"Exact match must win even if the wildcard certificate is newer."
|
||||
)
|
||||
|
||||
def test_wildcard_one_label_only(self):
|
||||
"""
|
||||
Wildcards (*.example.com) must only match one additional label.
|
||||
"""
|
||||
files = ["/etc/letsencrypt/live/wild/cert.pem"]
|
||||
text_wild = _san_block("*.example.com")
|
||||
|
||||
with patch.object(CertUtils, "list_cert_files", return_value=files), \
|
||||
patch.object(CertUtils, "run_openssl", return_value=text_wild), \
|
||||
patch.object(CertUtils, "run_openssl_dates", return_value=(50, 100000)), \
|
||||
patch("os.stat", side_effect=self._mock_stat_map({files[0]: 1000})):
|
||||
|
||||
# should match
|
||||
self.assertEqual(
|
||||
CertUtils.find_cert_for_domain("api.example.com", "/etc/letsencrypt/live"),
|
||||
"wild"
|
||||
)
|
||||
# too deep -> should not match
|
||||
self.assertIsNone(
|
||||
CertUtils.find_cert_for_domain("deep.api.example.com", "/etc/letsencrypt/live"),
|
||||
"Wildcard must not match multiple labels."
|
||||
)
|
||||
# base domain not covered
|
||||
self.assertIsNone(
|
||||
CertUtils.find_cert_for_domain("example.com", "/etc/letsencrypt/live"),
|
||||
"Base domain is not covered by *.example.com."
|
||||
)
|
||||
|
||||
def test_snapshot_refresh_rebuilds_mapping(self):
|
||||
"""
|
||||
ensure_cert_mapping() should rebuild mapping when snapshot changes.
|
||||
"""
|
||||
CertUtils._domain_cert_mapping = {"www.example.com": [{"folder": "old", "mtime": 1, "not_before": 1}]}
|
||||
|
||||
with patch.object(CertUtils, "snapshot_changed", return_value=True), \
|
||||
patch.object(CertUtils, "refresh_cert_mapping") as mock_refresh:
|
||||
|
||||
def _set_new_mapping(cert_base_path, debug=False):
|
||||
CertUtils._domain_cert_mapping = {
|
||||
"www.example.com": [{"folder": "new", "mtime": 999, "not_before": 999}]
|
||||
}
|
||||
|
||||
mock_refresh.side_effect = _set_new_mapping
|
||||
|
||||
folder = CertUtils.find_cert_for_domain("www.example.com", "/etc/letsencrypt/live", debug=False)
|
||||
self.assertEqual(folder, "new", "Mapping must be refreshed when snapshot changes.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user