"""Tests for certbot._internal.plugins.manual""" import sys import textwrap import unittest from unittest import mock import pytest from acme import challenges from certbot import errors from certbot.compat import filesystem from certbot.compat import os from certbot.tests import acme_util from certbot.tests import util as test_util class AuthenticatorTest(test_util.TempDirTestCase): """Tests for certbot._internal.plugins.manual.Authenticator.""" def setUp(self): super().setUp() get_display_patch = test_util.patch_display_util() self.mock_get_display = get_display_patch.start() self.addCleanup(get_display_patch.stop) self.http_achall = acme_util.HTTP01_A self.dns_achall = acme_util.DNS01_A self.dns_achall_2 = acme_util.DNS01_A_2 self.achalls = [self.http_achall, self.dns_achall, self.dns_achall_2] for d in ["config_dir", "work_dir", "in_progress"]: filesystem.mkdir(os.path.join(self.tempdir, d)) # "backup_dir" and "temp_checkpoint_dir" get created in # certbot.util.make_or_verify_dir() during the Reverter # initialization. self.config = mock.MagicMock( http01_port=0, manual_auth_hook=None, manual_cleanup_hook=None, noninteractive_mode=False, validate_hooks=False, config_dir=os.path.join(self.tempdir, "config_dir"), work_dir=os.path.join(self.tempdir, "work_dir"), backup_dir=os.path.join(self.tempdir, "backup_dir"), temp_checkpoint_dir=os.path.join( self.tempdir, "temp_checkpoint_dir"), in_progress_dir=os.path.join(self.tempdir, "in_progess")) from certbot._internal.plugins.manual import Authenticator self.auth = Authenticator(self.config, name='manual') def test_prepare_no_hook_noninteractive(self): self.config.noninteractive_mode = True with pytest.raises(errors.PluginError): self.auth.prepare() def test_prepare_bad_hook(self): self.config.manual_auth_hook = os.path.abspath(os.sep) # is / on UNIX self.config.validate_hooks = True with pytest.raises(errors.HookCommandNotFound): self.auth.prepare() def test_more_info(self): assert isinstance(self.auth.more_info(), str) def test_get_chall_pref(self): assert self.auth.get_chall_pref('example.org') == \ [challenges.HTTP01, challenges.DNS01] def test_script_perform(self): self.config.manual_auth_hook = ( '{0} -c "' 'from certbot.compat import os;' 'print(os.environ.get(\'CERTBOT_DOMAIN\'));' 'print(os.environ.get(\'CERTBOT_TOKEN\', \'notoken\'));' 'print(os.environ.get(\'CERTBOT_VALIDATION\', \'novalidation\'));' 'print(os.environ.get(\'CERTBOT_ALL_DOMAINS\'));' 'print(os.environ.get(\'CERTBOT_REMAINING_CHALLENGES\'));"' .format(sys.executable)) dns_expected = '{0}\n{1}\n{2}\n{3}\n{4}'.format( self.dns_achall.domain, 'notoken', self.dns_achall.validation(self.dns_achall.account_key), ','.join(achall.domain for achall in self.achalls), len(self.achalls) - self.achalls.index(self.dns_achall) - 1) http_expected = '{0}\n{1}\n{2}\n{3}\n{4}'.format( self.http_achall.domain, self.http_achall.chall.encode('token'), self.http_achall.validation(self.http_achall.account_key), ','.join(achall.domain for achall in self.achalls), len(self.achalls) - self.achalls.index(self.http_achall) - 1) assert self.auth.perform(self.achalls) == \ [achall.response(achall.account_key) for achall in self.achalls] assert self.auth.env[self.dns_achall]['CERTBOT_AUTH_OUTPUT'] == \ dns_expected assert self.auth.env[self.http_achall]['CERTBOT_AUTH_OUTPUT'] == \ http_expected # Successful hook output should be sent to notify assert self.mock_get_display().notification.call_count == len(self.achalls) for i, (args, _) in enumerate(self.mock_get_display().notification.call_args_list): needle = textwrap.indent(self.auth.env[self.achalls[i]]['CERTBOT_AUTH_OUTPUT'], ' ') assert needle in args[0] def test_manual_perform(self): assert self.auth.perform(self.achalls) == \ [achall.response(achall.account_key) for achall in self.achalls] assert self.mock_get_display().notification.call_count == len(self.achalls) for i, (args, kwargs) in enumerate(self.mock_get_display().notification.call_args_list): achall = self.achalls[i] assert achall.validation(achall.account_key) in args[0] assert kwargs['wrap'] is False def test_cleanup(self): self.config.manual_auth_hook = ('{0} -c "import sys; sys.stdout.write(\'foo\')"' .format(sys.executable)) self.config.manual_cleanup_hook = '# cleanup' self.auth.perform(self.achalls) for achall in self.achalls: self.auth.cleanup([achall]) assert os.environ['CERTBOT_AUTH_OUTPUT'] == 'foo' assert os.environ['CERTBOT_DOMAIN'] == achall.domain if isinstance(achall.chall, (challenges.HTTP01, challenges.DNS01)): assert os.environ['CERTBOT_VALIDATION'] == \ achall.validation(achall.account_key) if isinstance(achall.chall, challenges.HTTP01): assert os.environ['CERTBOT_TOKEN'] == \ achall.chall.encode('token') else: assert 'CERTBOT_TOKEN' not in os.environ def test_auth_hint_hook(self): self.config.manual_auth_hook = '/bin/true' assert self.auth.auth_hint([acme_util.DNS01_A, acme_util.HTTP01_A]) == \ 'The Certificate Authority failed to verify the DNS TXT records and challenge ' \ 'files created by the --manual-auth-hook. Ensure that this hook is functioning ' \ 'correctly and that it waits a sufficient duration of time for DNS propagation. ' \ 'Refer to "certbot --help manual" and the Certbot User Guide.' assert self.auth.auth_hint([acme_util.HTTP01_A]) == \ 'The Certificate Authority failed to verify the challenge files created by the ' \ '--manual-auth-hook. Ensure that this hook is functioning correctly. Refer to ' \ '"certbot --help manual" and the Certbot User Guide.' def test_auth_hint_no_hook(self): assert self.auth.auth_hint([acme_util.DNS01_A, acme_util.HTTP01_A]) == \ 'The Certificate Authority failed to verify the manually created DNS TXT records ' \ 'and challenge files. Ensure that you created these in the correct location, or ' \ 'try waiting longer for DNS propagation on the next attempt.' assert self.auth.auth_hint([acme_util.HTTP01_A, acme_util.HTTP01_A, acme_util.HTTP01_A]) == \ 'The Certificate Authority failed to verify the manually created challenge files. ' \ 'Ensure that you created these in the correct location.' if __name__ == '__main__': sys.exit(pytest.main(sys.argv[1:] + [__file__])) # pragma: no cover