mirror of
https://github.com/certbot/certbot.git
synced 2026-04-28 01:28:25 -04:00
* lineage_for_certname should return None if there is no existing renewal file * add unit test * add regression test to integration test * revent boulder-start to boulder-fetch
474 lines
19 KiB
Python
474 lines
19 KiB
Python
"""Tests for certbot.cert_manager."""
|
|
# pylint: disable=protected-access
|
|
import os
|
|
import re
|
|
import shutil
|
|
import tempfile
|
|
import unittest
|
|
|
|
import configobj
|
|
import mock
|
|
|
|
from certbot import configuration
|
|
from certbot import errors
|
|
|
|
from certbot.display import util as display_util
|
|
from certbot.storage import ALL_FOUR
|
|
|
|
from certbot.tests import storage_test
|
|
from certbot.tests import util as test_util
|
|
|
|
class BaseCertManagerTest(unittest.TestCase):
|
|
"""Base class for setting up Cert Manager tests.
|
|
"""
|
|
def setUp(self):
|
|
self.tempdir = tempfile.mkdtemp()
|
|
|
|
os.makedirs(os.path.join(self.tempdir, "renewal"))
|
|
|
|
self.cli_config = configuration.NamespaceConfig(mock.MagicMock(
|
|
config_dir=self.tempdir,
|
|
work_dir=self.tempdir,
|
|
logs_dir=self.tempdir,
|
|
quiet=False,
|
|
))
|
|
|
|
self.domains = {
|
|
"example.org": None,
|
|
"other.com": os.path.join(self.tempdir, "specialarchive")
|
|
}
|
|
self.configs = dict((domain, self._set_up_config(domain, self.domains[domain]))
|
|
for domain in self.domains)
|
|
|
|
# We also create a file that isn't a renewal config in the same
|
|
# location to test that logic that reads in all-and-only renewal
|
|
# configs will ignore it and NOT attempt to parse it.
|
|
junk = open(os.path.join(self.tempdir, "renewal", "IGNORE.THIS"), "w")
|
|
junk.write("This file should be ignored!")
|
|
junk.close()
|
|
|
|
def _set_up_config(self, domain, custom_archive):
|
|
# TODO: maybe provide NamespaceConfig.make_dirs?
|
|
# TODO: main() should create those dirs, c.f. #902
|
|
os.makedirs(os.path.join(self.tempdir, "live", domain))
|
|
config = configobj.ConfigObj()
|
|
|
|
if custom_archive is not None:
|
|
os.makedirs(custom_archive)
|
|
config["archive_dir"] = custom_archive
|
|
else:
|
|
os.makedirs(os.path.join(self.tempdir, "archive", domain))
|
|
|
|
for kind in ALL_FOUR:
|
|
config[kind] = os.path.join(self.tempdir, "live", domain,
|
|
kind + ".pem")
|
|
|
|
config.filename = os.path.join(self.tempdir, "renewal",
|
|
domain + ".conf")
|
|
config.write()
|
|
return config
|
|
|
|
def tearDown(self):
|
|
shutil.rmtree(self.tempdir)
|
|
|
|
|
|
class UpdateLiveSymlinksTest(BaseCertManagerTest):
|
|
"""Tests for certbot.cert_manager.update_live_symlinks
|
|
"""
|
|
def test_update_live_symlinks(self):
|
|
"""Test update_live_symlinks"""
|
|
# pylint: disable=too-many-statements
|
|
# create files with incorrect symlinks
|
|
from certbot import cert_manager
|
|
archive_paths = {}
|
|
for domain in self.domains:
|
|
custom_archive = self.domains[domain]
|
|
if custom_archive is not None:
|
|
archive_dir_path = custom_archive
|
|
else:
|
|
archive_dir_path = os.path.join(self.tempdir, "archive", domain)
|
|
archive_paths[domain] = dict((kind,
|
|
os.path.join(archive_dir_path, kind + "1.pem")) for kind in ALL_FOUR)
|
|
for kind in ALL_FOUR:
|
|
live_path = self.configs[domain][kind]
|
|
archive_path = archive_paths[domain][kind]
|
|
open(archive_path, 'a').close()
|
|
# path is incorrect but base must be correct
|
|
os.symlink(os.path.join(self.tempdir, kind + "1.pem"), live_path)
|
|
|
|
# run update symlinks
|
|
cert_manager.update_live_symlinks(self.cli_config)
|
|
|
|
# check that symlinks go where they should
|
|
prev_dir = os.getcwd()
|
|
try:
|
|
for domain in self.domains:
|
|
for kind in ALL_FOUR:
|
|
os.chdir(os.path.dirname(self.configs[domain][kind]))
|
|
self.assertEqual(
|
|
os.path.realpath(os.readlink(self.configs[domain][kind])),
|
|
os.path.realpath(archive_paths[domain][kind]))
|
|
finally:
|
|
os.chdir(prev_dir)
|
|
|
|
|
|
class DeleteTest(storage_test.BaseRenewableCertTest):
|
|
"""Tests for certbot.cert_manager.delete
|
|
"""
|
|
@test_util.patch_get_utility()
|
|
@mock.patch('certbot.cert_manager.lineage_for_certname')
|
|
@mock.patch('certbot.storage.delete_files')
|
|
def test_delete(self, mock_delete_files, mock_lineage_for_certname, unused_get_utility):
|
|
"""Test delete"""
|
|
mock_lineage_for_certname.return_value = self.test_rc
|
|
self.cli_config.certname = "example.org"
|
|
from certbot import cert_manager
|
|
cert_manager.delete(self.cli_config)
|
|
self.assertTrue(mock_delete_files.called)
|
|
|
|
|
|
class CertificatesTest(BaseCertManagerTest):
|
|
"""Tests for certbot.cert_manager.certificates
|
|
"""
|
|
def _certificates(self, *args, **kwargs):
|
|
from certbot.cert_manager import certificates
|
|
return certificates(*args, **kwargs)
|
|
|
|
@mock.patch('certbot.cert_manager.logger')
|
|
@test_util.patch_get_utility()
|
|
def test_certificates_parse_fail(self, mock_utility, mock_logger):
|
|
self._certificates(self.cli_config)
|
|
self.assertTrue(mock_logger.warning.called) #pylint: disable=no-member
|
|
self.assertTrue(mock_utility.called)
|
|
|
|
@mock.patch('certbot.cert_manager.logger')
|
|
@test_util.patch_get_utility()
|
|
def test_certificates_quiet(self, mock_utility, mock_logger):
|
|
self.cli_config.quiet = True
|
|
self._certificates(self.cli_config)
|
|
self.assertFalse(mock_utility.notification.called)
|
|
self.assertTrue(mock_logger.warning.called) #pylint: disable=no-member
|
|
|
|
@mock.patch('certbot.cert_manager.logger')
|
|
@test_util.patch_get_utility()
|
|
@mock.patch("certbot.storage.RenewableCert")
|
|
@mock.patch('certbot.cert_manager._report_human_readable')
|
|
def test_certificates_parse_success(self, mock_report, mock_renewable_cert,
|
|
mock_utility, mock_logger):
|
|
mock_report.return_value = ""
|
|
self._certificates(self.cli_config)
|
|
self.assertFalse(mock_logger.warning.called) #pylint: disable=no-member
|
|
self.assertTrue(mock_report.called)
|
|
self.assertTrue(mock_utility.called)
|
|
self.assertTrue(mock_renewable_cert.called)
|
|
|
|
@mock.patch('certbot.cert_manager.logger')
|
|
@test_util.patch_get_utility()
|
|
def test_certificates_no_files(self, mock_utility, mock_logger):
|
|
tempdir = tempfile.mkdtemp()
|
|
|
|
cli_config = configuration.NamespaceConfig(mock.MagicMock(
|
|
config_dir=tempdir,
|
|
work_dir=tempdir,
|
|
logs_dir=tempdir,
|
|
quiet=False,
|
|
))
|
|
|
|
os.makedirs(os.path.join(tempdir, "renewal"))
|
|
self._certificates(cli_config)
|
|
self.assertFalse(mock_logger.warning.called) #pylint: disable=no-member
|
|
self.assertTrue(mock_utility.called)
|
|
shutil.rmtree(tempdir)
|
|
|
|
@mock.patch('certbot.cert_manager.ocsp.RevocationChecker.ocsp_revoked')
|
|
def test_report_human_readable(self, mock_revoked):
|
|
mock_revoked.return_value = None
|
|
from certbot import cert_manager
|
|
import datetime, pytz
|
|
expiry = pytz.UTC.fromutc(datetime.datetime.utcnow())
|
|
|
|
cert = mock.MagicMock(lineagename="nameone")
|
|
cert.target_expiry = expiry
|
|
cert.names.return_value = ["nameone", "nametwo"]
|
|
cert.is_test_cert = False
|
|
parsed_certs = [cert]
|
|
|
|
# pylint: disable=protected-access
|
|
get_report = lambda: cert_manager._report_human_readable(mock_config, parsed_certs)
|
|
|
|
mock_config = mock.MagicMock(certname=None, lineagename=None)
|
|
# pylint: disable=protected-access
|
|
out = get_report()
|
|
self.assertTrue("INVALID: EXPIRED" in out)
|
|
|
|
cert.target_expiry += datetime.timedelta(hours=2)
|
|
# pylint: disable=protected-access
|
|
out = get_report()
|
|
self.assertTrue('1 hour(s)' in out)
|
|
self.assertTrue('VALID' in out and not 'INVALID' in out)
|
|
|
|
cert.target_expiry += datetime.timedelta(days=1)
|
|
# pylint: disable=protected-access
|
|
out = get_report()
|
|
self.assertTrue('1 day' in out)
|
|
self.assertFalse('under' in out)
|
|
self.assertTrue('VALID' in out and not 'INVALID' in out)
|
|
|
|
cert.target_expiry += datetime.timedelta(days=2)
|
|
# pylint: disable=protected-access
|
|
out = get_report()
|
|
self.assertTrue('3 days' in out)
|
|
self.assertTrue('VALID' in out and not 'INVALID' in out)
|
|
|
|
cert.is_test_cert = True
|
|
mock_revoked.return_value = True
|
|
out = get_report()
|
|
self.assertTrue('INVALID: TEST_CERT, REVOKED' in out)
|
|
|
|
cert = mock.MagicMock(lineagename="indescribable")
|
|
cert.target_expiry = expiry
|
|
cert.names.return_value = ["nameone", "thrice.named"]
|
|
cert.is_test_cert = True
|
|
parsed_certs.append(cert)
|
|
|
|
out = get_report()
|
|
self.assertEqual(len(re.findall("INVALID:", out)), 2)
|
|
mock_config.domains = ["thrice.named"]
|
|
out = get_report()
|
|
self.assertEqual(len(re.findall("INVALID:", out)), 1)
|
|
mock_config.domains = ["nameone"]
|
|
out = get_report()
|
|
self.assertEqual(len(re.findall("INVALID:", out)), 2)
|
|
mock_config.certname = "indescribable"
|
|
out = get_report()
|
|
self.assertEqual(len(re.findall("INVALID:", out)), 1)
|
|
mock_config.certname = "horror"
|
|
out = get_report()
|
|
self.assertEqual(len(re.findall("INVALID:", out)), 0)
|
|
|
|
|
|
class SearchLineagesTest(BaseCertManagerTest):
|
|
"""Tests for certbot.cert_manager._search_lineages."""
|
|
|
|
@mock.patch('certbot.util.make_or_verify_dir')
|
|
@mock.patch('certbot.storage.renewal_conf_files')
|
|
@mock.patch('certbot.storage.RenewableCert')
|
|
def test_cert_storage_error(self, mock_renewable_cert, mock_renewal_conf_files,
|
|
mock_make_or_verify_dir):
|
|
mock_renewal_conf_files.return_value = ["badfile"]
|
|
mock_renewable_cert.side_effect = errors.CertStorageError
|
|
from certbot import cert_manager
|
|
# pylint: disable=protected-access
|
|
self.assertEqual(cert_manager._search_lineages(self.cli_config, lambda x: x, "check"),
|
|
"check")
|
|
self.assertTrue(mock_make_or_verify_dir.called)
|
|
|
|
|
|
class LineageForCertnameTest(BaseCertManagerTest):
|
|
"""Tests for certbot.cert_manager.lineage_for_certname"""
|
|
|
|
@mock.patch('certbot.util.make_or_verify_dir')
|
|
@mock.patch('certbot.storage.renewal_file_for_certname')
|
|
@mock.patch('certbot.storage.RenewableCert')
|
|
def test_found_match(self, mock_renewable_cert, mock_renewal_conf_file,
|
|
mock_make_or_verify_dir):
|
|
mock_renewal_conf_file.return_value = "somefile.conf"
|
|
mock_match = mock.Mock(lineagename="example.com")
|
|
mock_renewable_cert.return_value = mock_match
|
|
from certbot import cert_manager
|
|
self.assertEqual(cert_manager.lineage_for_certname(self.cli_config, "example.com"),
|
|
mock_match)
|
|
self.assertTrue(mock_make_or_verify_dir.called)
|
|
|
|
@mock.patch('certbot.util.make_or_verify_dir')
|
|
@mock.patch('certbot.storage.renewal_file_for_certname')
|
|
def test_no_match(self, mock_renewal_conf_file,
|
|
mock_make_or_verify_dir):
|
|
mock_renewal_conf_file.return_value = "other.com.conf"
|
|
from certbot import cert_manager
|
|
self.assertEqual(cert_manager.lineage_for_certname(self.cli_config, "example.com"),
|
|
None)
|
|
self.assertTrue(mock_make_or_verify_dir.called)
|
|
|
|
@mock.patch('certbot.util.make_or_verify_dir')
|
|
@mock.patch('certbot.storage.renewal_file_for_certname')
|
|
def test_no_renewal_file(self, mock_renewal_conf_file,
|
|
mock_make_or_verify_dir):
|
|
mock_renewal_conf_file.side_effect = errors.CertStorageError()
|
|
from certbot import cert_manager
|
|
self.assertEqual(cert_manager.lineage_for_certname(self.cli_config, "example.com"),
|
|
None)
|
|
self.assertTrue(mock_make_or_verify_dir.called)
|
|
|
|
|
|
class DomainsForCertnameTest(BaseCertManagerTest):
|
|
"""Tests for certbot.cert_manager.domains_for_certname"""
|
|
|
|
@mock.patch('certbot.util.make_or_verify_dir')
|
|
@mock.patch('certbot.storage.renewal_file_for_certname')
|
|
@mock.patch('certbot.storage.RenewableCert')
|
|
def test_found_match(self, mock_renewable_cert, mock_renewal_conf_file,
|
|
mock_make_or_verify_dir):
|
|
mock_renewal_conf_file.return_value = "somefile.conf"
|
|
mock_match = mock.Mock(lineagename="example.com")
|
|
domains = ["example.com", "example.org"]
|
|
mock_match.names.return_value = domains
|
|
mock_renewable_cert.return_value = mock_match
|
|
from certbot import cert_manager
|
|
self.assertEqual(cert_manager.domains_for_certname(self.cli_config, "example.com"),
|
|
domains)
|
|
self.assertTrue(mock_make_or_verify_dir.called)
|
|
|
|
@mock.patch('certbot.util.make_or_verify_dir')
|
|
@mock.patch('certbot.storage.renewal_file_for_certname')
|
|
def test_no_match(self, mock_renewal_conf_file,
|
|
mock_make_or_verify_dir):
|
|
mock_renewal_conf_file.return_value = "somefile.conf"
|
|
from certbot import cert_manager
|
|
self.assertEqual(cert_manager.domains_for_certname(self.cli_config, "other.com"),
|
|
None)
|
|
self.assertTrue(mock_make_or_verify_dir.called)
|
|
|
|
|
|
class RenameLineageTest(BaseCertManagerTest):
|
|
"""Tests for certbot.cert_manager.rename_lineage"""
|
|
|
|
def setUp(self):
|
|
super(RenameLineageTest, self).setUp()
|
|
self.mock_config = configuration.NamespaceConfig(
|
|
namespace=mock.MagicMock(
|
|
config_dir=self.tempdir,
|
|
work_dir=self.tempdir,
|
|
logs_dir=self.tempdir,
|
|
certname="example.org",
|
|
new_certname="after",
|
|
)
|
|
)
|
|
|
|
def _call(self, *args, **kwargs):
|
|
from certbot import cert_manager
|
|
return cert_manager.rename_lineage(*args, **kwargs)
|
|
|
|
@mock.patch('certbot.storage.renewal_conf_files')
|
|
@test_util.patch_get_utility()
|
|
def test_no_certname(self, mock_get_utility, mock_renewal_conf_files):
|
|
mock_config = mock.Mock(certname=None, new_certname="two")
|
|
|
|
# if not choices
|
|
mock_renewal_conf_files.return_value = []
|
|
self.assertRaises(errors.Error, self._call, mock_config)
|
|
|
|
mock_renewal_conf_files.return_value = ["one.conf"]
|
|
util_mock = mock.Mock()
|
|
util_mock.menu.return_value = (display_util.CANCEL, 0)
|
|
mock_get_utility.return_value = util_mock
|
|
self.assertRaises(errors.Error, self._call, mock_config)
|
|
|
|
util_mock.menu.return_value = (display_util.OK, -1)
|
|
self.assertRaises(errors.Error, self._call, mock_config)
|
|
|
|
@test_util.patch_get_utility()
|
|
def test_no_new_certname(self, mock_get_utility):
|
|
mock_config = mock.Mock(certname="one", new_certname=None)
|
|
|
|
util_mock = mock.Mock()
|
|
util_mock.input.return_value = (display_util.CANCEL, "name")
|
|
mock_get_utility.return_value = util_mock
|
|
self.assertRaises(errors.Error, self._call, mock_config)
|
|
|
|
util_mock = mock.Mock()
|
|
util_mock.input.return_value = (display_util.OK, None)
|
|
mock_get_utility.return_value = util_mock
|
|
self.assertRaises(errors.Error, self._call, mock_config)
|
|
|
|
@test_util.patch_get_utility()
|
|
@mock.patch('certbot.cert_manager.lineage_for_certname')
|
|
def test_no_existing_certname(self, mock_lineage_for_certname, unused_get_utility):
|
|
mock_config = mock.Mock(certname="one", new_certname="two")
|
|
mock_lineage_for_certname.return_value = None
|
|
self.assertRaises(errors.ConfigurationError,
|
|
self._call, mock_config)
|
|
|
|
@test_util.patch_get_utility()
|
|
@mock.patch("certbot.storage.RenewableCert._check_symlinks")
|
|
def test_rename_cert(self, mock_check, unused_get_utility):
|
|
mock_check.return_value = True
|
|
mock_config = self.mock_config
|
|
self._call(mock_config)
|
|
from certbot import cert_manager
|
|
updated_lineage = cert_manager.lineage_for_certname(mock_config, mock_config.new_certname)
|
|
self.assertTrue(updated_lineage is not None)
|
|
self.assertEqual(updated_lineage.lineagename, mock_config.new_certname)
|
|
|
|
@test_util.patch_get_utility()
|
|
@mock.patch("certbot.storage.RenewableCert._check_symlinks")
|
|
def test_rename_cert_interactive_certname(self, mock_check, mock_get_utility):
|
|
mock_check.return_value = True
|
|
mock_config = self.mock_config
|
|
mock_config.certname = None
|
|
util_mock = mock.Mock()
|
|
util_mock.menu.return_value = (display_util.OK, 0)
|
|
mock_get_utility.return_value = util_mock
|
|
self._call(mock_config)
|
|
from certbot import cert_manager
|
|
updated_lineage = cert_manager.lineage_for_certname(mock_config, mock_config.new_certname)
|
|
self.assertTrue(updated_lineage is not None)
|
|
self.assertEqual(updated_lineage.lineagename, mock_config.new_certname)
|
|
|
|
@test_util.patch_get_utility()
|
|
@mock.patch("certbot.storage.RenewableCert._check_symlinks")
|
|
def test_rename_cert_bad_new_certname(self, mock_check, unused_get_utility):
|
|
mock_check.return_value = True
|
|
mock_config = self.mock_config
|
|
|
|
# for example, don't rename to existing certname
|
|
mock_config.new_certname = "example.org"
|
|
self.assertRaises(errors.ConfigurationError, self._call, mock_config)
|
|
|
|
mock_config.new_certname = "one{0}two".format(os.path.sep)
|
|
self.assertRaises(errors.ConfigurationError, self._call, mock_config)
|
|
|
|
|
|
class DuplicativeCertsTest(storage_test.BaseRenewableCertTest):
|
|
"""Test to avoid duplicate lineages."""
|
|
|
|
def setUp(self):
|
|
super(DuplicativeCertsTest, self).setUp()
|
|
self.config.write()
|
|
self._write_out_ex_kinds()
|
|
|
|
def tearDown(self):
|
|
shutil.rmtree(self.tempdir)
|
|
|
|
@mock.patch('certbot.util.make_or_verify_dir')
|
|
def test_find_duplicative_names(self, unused_makedir):
|
|
from certbot.cert_manager import find_duplicative_certs
|
|
test_cert = test_util.load_vector('cert-san.pem')
|
|
with open(self.test_rc.cert, 'wb') as f:
|
|
f.write(test_cert)
|
|
|
|
# No overlap at all
|
|
result = find_duplicative_certs(
|
|
self.cli_config, ['wow.net', 'hooray.org'])
|
|
self.assertEqual(result, (None, None))
|
|
|
|
# Totally identical
|
|
result = find_duplicative_certs(
|
|
self.cli_config, ['example.com', 'www.example.com'])
|
|
self.assertTrue(result[0].configfile.filename.endswith('example.org.conf'))
|
|
self.assertEqual(result[1], None)
|
|
|
|
# Superset
|
|
result = find_duplicative_certs(
|
|
self.cli_config, ['example.com', 'www.example.com', 'something.new'])
|
|
self.assertEqual(result[0], None)
|
|
self.assertTrue(result[1].configfile.filename.endswith('example.org.conf'))
|
|
|
|
# Partial overlap doesn't count
|
|
result = find_duplicative_certs(
|
|
self.cli_config, ['example.com', 'something.new'])
|
|
self.assertEqual(result, (None, None))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main() # pragma: no cover
|