diff --git a/letsencrypt/client/display/ops.py b/letsencrypt/client/display/ops.py index 24b9dc1d5..5a77e0ffb 100644 --- a/letsencrypt/client/display/ops.py +++ b/letsencrypt/client/display/ops.py @@ -15,30 +15,49 @@ util = zope.component.getUtility # pylint: disable=invalid-name def choose_plugin(prepared, question): """Allow the user to choose ther plugin. - :param list prepared: + :param list prepared: List of `~.PluginEntryPoint`. + :param str question: Question to be presented to the user. + + :returns: Plugin entry point chosen by the user. + :rtype: `~.PluginEntryPoint` """ opts = [plugin_ep.name_with_description + (" [Misconfigured]" if plugin_ep.misconfigured else "") - for plugin_ep in prepared.itervalues()] + for plugin_ep in prepared] while True: code, index = util(interfaces.IDisplay).menu( question, opts, help_label="More Info") if code == display_util.OK: - return prepared[index][0] + return prepared[index] elif code == display_util.HELP: - if prepared[index][1] is not None: + if prepared[index].misconfigured: msg = "Reported Error: %s" % prepared[index].prepare() else: - msg = prepared[index][0].init().more_info() + msg = prepared[index].init().more_info() util(interfaces.IDisplay).notification( msg, height=display_util.HEIGHT) else: return None -def _pick_plugin(config, default, plugins, question, ifaces): + +def pick_plugin(config, default, plugins, question, ifaces): + """Pick plugin. + + :param letsencrypt.client.interfaces.IConfig: Configuration + :param str default: Plugin name supplied by user or ``None``. + :param letsencrypt.client.plugins.disco.PluginsRegistry plugins: + All plugins registered as entry points. + :param str question: Question to be presented to the user in case + multiple candidates are found. + :param list ifaces: Interfaces that plugins must provide. + + :returns: Initialized plugin. + :rtype: IPlugin + + """ if default is not None: # throw more UX-friendly error if default not in plugins filtered = plugins.filter(lambda p_ep: p_ep.name == default) @@ -47,8 +66,8 @@ def _pick_plugin(config, default, plugins, question, ifaces): filtered.init(config) verified = filtered.verify(ifaces) - filtered.prepare() - prepared = filtered.available() + verified.prepare() + prepared = verified.available() if len(prepared) > 1: logging.debug("Multiple candidate plugins: %s", prepared) @@ -66,14 +85,14 @@ def pick_authenticator( config, default, plugins, question="How would you " "like to authenticate with Let's Encrypt CA?"): """Pick authentication plugin.""" - return _pick_plugin( + return pick_plugin( config, default, plugins, question, (interfaces.IAuthenticator,)) def pick_installer(config, default, plugins, question="How would you like to install certificates?"): """Pick installer plugin.""" - return _pick_plugin( + return pick_plugin( config, default, plugins, question, (interfaces.IInstaller,)) @@ -82,7 +101,7 @@ def pick_configurator( question="How would you like to authenticate and install " "certificates?"): """Pick configurator plugin.""" - return _pick_plugin( + return pick_plugin( config, default, plugins, question, (interfaces.IAuthenticator, interfaces.IInstaller)) diff --git a/letsencrypt/client/tests/display/ops_test.py b/letsencrypt/client/tests/display/ops_test.py index 2da411b6b..151358f8a 100644 --- a/letsencrypt/client/tests/display/ops_test.py +++ b/letsencrypt/client/tests/display/ops_test.py @@ -8,10 +8,129 @@ import mock import zope.component from letsencrypt.client import account +from letsencrypt.client import interfaces from letsencrypt.client import le_util + from letsencrypt.client.display import util as display_util +class ChoosePluginTest(unittest.TestCase): + """Tests for letsencrypt.client.display.ops.choose_plugin.""" + + def setUp(self): + zope.component.provideUtility(display_util.FileDisplay(sys.stdout)) + self.mock_apache = mock.Mock( + name_with_description="a", misconfigured=True) + self.mock_stand = mock.Mock( + name_with_description="s", misconfigured=False) + self.mock_stand.init().more_info.return_value = "standalone" + self.plugins = [ + self.mock_apache, + self.mock_stand, + ] + + def _call(self): + from letsencrypt.client.display.ops import choose_plugin + return choose_plugin(self.plugins, "Question?") + + @mock.patch("letsencrypt.client.display.ops.util") + def test_successful_choice(self, mock_util): + mock_util().menu.return_value = (display_util.OK, 0) + self.assertEqual(self.mock_apache, self._call()) + + @mock.patch("letsencrypt.client.display.ops.util") + def test_more_info(self, mock_util): + mock_util().menu.side_effect = [ + (display_util.HELP, 0), + (display_util.HELP, 1), + (display_util.OK, 1), + ] + + self.assertEqual(self.mock_stand, self._call()) + self.assertEqual(mock_util().notification.call_count, 2) + + @mock.patch("letsencrypt.client.display.ops.util") + def test_no_choice(self, mock_util): + mock_util().menu.return_value = (display_util.CANCEL, 0) + self.assertTrue(self._call() is None) + + +class PickPluginTest(unittest.TestCase): + """Tests for letsencrypt.client.display.ops.pick_plugin.""" + + def setUp(self): + self.config = mock.Mock() + self.default = None + self.reg = mock.MagicMock() + self.question = "Question?" + self.ifaces = [] + + def _call(self): + from letsencrypt.client.display.ops import pick_plugin + return pick_plugin(self.config, self.default, self.reg, + self.question, self.ifaces) + + def test_default_provided(self): + self.default = "foo" + self._call() + self.reg.filter.assert_called_once() + + def test_no_default(self): + self._call() + self.reg.filter.assert_called_once() + + def test_no_candidate(self): + self.assertTrue(self._call() is None) + + def test_single(self): + plugin_ep = mock.MagicMock() + plugin_ep.init.return_value = "foo" + self.reg.ifaces().verify().available.return_value = {"bar": plugin_ep} + self.assertEqual("foo", self._call()) + + def test_multiple(self): + plugin_ep = mock.MagicMock() + plugin_ep.init.return_value = "foo" + self.reg.ifaces().verify().available.return_value = { + "bar": plugin_ep, + "baz": plugin_ep, + } + with mock.patch("letsencrypt.client.display" + ".ops.choose_plugin") as mock_choose: + mock_choose.return_value = plugin_ep + self.assertEqual("foo", self._call()) + mock_choose.assert_called_once_with( + [plugin_ep, plugin_ep], self.question) + + +class ConveniencePickPluginTest(unittest.TestCase): + """Tests for letsencrypt.client.display.ops.pick_*.""" + + def _test(self, fun, ifaces): + config = mock.Mock() + default = mock.Mock() + plugins = mock.Mock() + + with mock.patch("letsencrypt.client.display.ops.pick_plugin") as mock_p: + mock_p.return_value = "foo" + self.assertEqual("foo", fun(config, default, plugins, "Question?")) + mock_p.assert_called_once_with( + config, default, plugins, "Question?", ifaces) + + def test_authenticator(self): + from letsencrypt.client.display.ops import pick_authenticator + self._test(pick_authenticator, (interfaces.IAuthenticator,)) + + def test_installer(self): + from letsencrypt.client.display.ops import pick_installer + self._test(pick_installer, (interfaces.IInstaller,)) + + def test_configurator(self): + from letsencrypt.client.display.ops import pick_configurator + self._test(pick_configurator, ( + interfaces.IAuthenticator, interfaces.IInstaller)) + + class ChooseAccountTest(unittest.TestCase): """Test choose_account.""" def setUp(self):