From 01899f387e06d4f1a57918d55deadc55db216fa1 Mon Sep 17 00:00:00 2001 From: James Kasten Date: Sun, 8 Feb 2015 21:02:23 -0800 Subject: [PATCH] Unittests for display_util --- letsencrypt/client/display.py | 311 ------------------ letsencrypt/client/display/display_util.py | 54 +-- letsencrypt/client/enhance_display.py | 67 ---- letsencrypt/client/interfaces.py | 21 +- .../client/tests/display/display_util_test.py | 254 +++++++++++++- 5 files changed, 292 insertions(+), 415 deletions(-) delete mode 100644 letsencrypt/client/display.py delete mode 100644 letsencrypt/client/enhance_display.py diff --git a/letsencrypt/client/display.py b/letsencrypt/client/display.py deleted file mode 100644 index 7f2f67a21..000000000 --- a/letsencrypt/client/display.py +++ /dev/null @@ -1,311 +0,0 @@ -"""Lets Encrypt display.""" -import os -import textwrap - -import dialog -import zope.interface - -from letsencrypt.client import interfaces - - -WIDTH = 72 -HEIGHT = 20 - - -class NcursesDisplay(object): - """Ncurses-based display.""" - - zope.interface.implements(interfaces.IDisplay) - - def __init__(self, width=WIDTH, height=HEIGHT): - super(NcursesDisplay, self).__init__() - self.dialog = dialog.Dialog() - self.width = width - self.height = height - - def notification(self, message, height=10): - """Display a notification to the user and wait for user acceptance. - - :param str message: Message to display - :param int height: Height of the dialog box - - """ - self.dialog.msgbox(message, height, width=self.width) - - def menu(self, message, choices, unused_input_text="", - ok_label="OK", help_label=""): - """Display a menu. - - :param str message: title of menu - :param choices: menu lines - :type choices: list of tuples (tag, item) or - list of items (tags will be enumerated) - - :returns: tuple of the form (code, tag) where - code is a display exit code - tag is the tag string corresponding to the item chosen - :rtype: tuple - - """ - if help_label: - help_button = True - else: - help_button = False - - # Can accept either tuples or just the actual choices - if choices and isinstance(choices[0], tuple): - code, selection = self.dialog.menu( - message, choices=choices, ok_label=ok_label, - help_button=help_button, help_label=help_label, - width=self.width, height=self.height) - - return code, str(selection) - else: - choices = list(enumerate(choices, 1)) - code, tag = self.dialog.menu( - message, choices=choices, ok_label=ok_label, - help_button=help_button, help_label=help_label, - width=self.width, height=self.height) - - return code, int(tag) - 1 - - def input(self, message): - """Display an input box to the user. - - :param str message: Message to display that asks for input. - - :returns: tuple of the form (code, string) where - code is a display exit code - string is the input entered by the user - - """ - return self.dialog.inputbox(message) - - def yesno(self, message, yes_label="Yes", no_label="No"): - """Display a Yes/No dialog box - - :param str message: message to display to user - :param str yes_label: label on the "yes" button - :param str no_label: label on the "no" button - - :returns: if yes_label was selected - :rtype: bool - - """ - return self.dialog.DIALOG_OK == self.dialog.yesno( - message, self.height, self.width, - yes_label=yes_label, no_label=no_label) - - def filter_names(self, names): - """Determine which names the user would like to select from a list. - - :param list names: domain names - - :returns: tuple of the form (code, names) where - code is a display exit code - names is a list of names selected - :rtype: tuple - - """ - choices = [(n, "", 0) for n in names] - code, names = self.dialog.checklist( - "Which names would you like to activate HTTPS for?", - choices=choices) - return code, [str(s) for s in names] - - def success_installation(self, domains): - """Display a box confirming the installation of HTTPS. - - :param list domains: domain names which were enabled - - """ - self.dialog.msgbox( - "\nCongratulations! You have successfully enabled " - + gen_https_names(domains) + "!", width=self.width) - - -class FileDisplay(object): - """File-based display.""" - - zope.interface.implements(interfaces.IDisplay) - - def __init__(self, outfile): - super(FileDisplay, self).__init__() - self.outfile = outfile - - def notification(self, message, unused_height): - """Displays a notification and waits for user acceptance. - - :param str message: Message to display - - """ - side_frame = "-" * 79 - lines = message.splitlines() - fixed_l = [] - for line in lines: - fixed_l.append(textwrap.fill(line, 80)) - self.outfile.write( - "{0}{1}{0}{2}{0}{1}{0}".format( - os.linesep, side_frame, os.linesep.join(fixed_l))) - raw_input("Press Enter to Continue") - - def menu(self, message, choices, input_text="", - unused_ok_label = "", unused_help_label=""): - """Display a menu. - - :param str message: title of menu - :param choices: Menu lines - :type choices: list of tuples (tag, item) or - list of items (tags will be enumerated) - - :returns: tuple of the form (code, tag) where - code is a display exit code - tag is the tag string corresponding to the item chosen - :rtype: tuple - - """ - # Can take either tuples or single items in choices list - if choices and isinstance(choices[0], tuple): - choices = ["%s - %s" % (c[0], c[1]) for c in choices] - - self.outfile.write("\n%s\n" % message) - side_frame = "-" * 79 - self.outfile.write("%s\n" % side_frame) - - for i, choice in enumerate(choices, 1): - self.outfile.write(textwrap.fill( - "%d: %s" % (i, choice), 80) + "\n") - - self.outfile.write("%s\n" % side_frame) - - code, selection = self._get_valid_int_ans( - "%s (c to cancel): " % input_text) - - return code, (selection - 1) - - def input(self, message): - # pylint: disable=no-self-use - """Accept input from the user - - :param str message: message to display to the user - - :returns: tuple of (code, input) where - code is a display exit code - input is a str of the user's input - :rtype: tuple - - """ - ans = raw_input("%s (Enter c to cancel)\n" % message) - - if ans == "c" or ans == "C": - return CANCEL, "-1" - else: - return OK, ans - - def yesno(self, message, unused_yes_label="", unused_no_label=""): - """Query the user with a yes/no question. - - :param str message: question for the user - - :returns: True for "Yes", False for "No" - :rtype: bool - - """ - self.outfile.write("\n%s\n" % textwrap.fill(message, 80)) - ans = raw_input("y/n: ") - return ans.startswith("y") or ans.startswith("Y") - - def filter_names(self, names): - """Determine which names the user would like to select from a list. - - :param list names: domain names - - :returns: tuple of the form (code, names) where - code is a display exit code - names is a list of names selected - :rtype: tuple - - """ - code, tag = self.menu( - "Choose the names would you like to upgrade to HTTPS?", - names, "Select the number of the name: ") - - # Make sure to return a list... - return code, [names[tag]] - - def success_installation(self, domains): - """Display a box confirming the installation of HTTPS. - - :param list domains: domain names which were enabled - - """ - side_frame = '*' * 79 - msg = textwrap.fill("Congratulations! You have successfully " - "enabled %s!" % gen_https_names(domains)) - self.outfile.write("%s\n%s\n%s\n" % (side_frame, msg, side_frame)) - - - def _get_valid_int_ans(self, input_string): - """Get a numerical selection. - - :param str input_string: Instructions for the user to make a selection. - - :returns: tuple of the form (code, selection) where - code is a display exit code - selection is the user"s int selection - :rtype: tuple - - """ - valid_ans = False - e_msg = "Make a selection by inputting the appropriate number.\n" - while not valid_ans: - - ans = raw_input(input_string) - if ans.startswith("c") or ans.startswith("C"): - code = CANCEL - selection = -1 - valid_ans = True - else: - try: - selection = int(ans) - # TODO add check to make sure it is less than max - if selection < 0: - self.outfile.write(e_msg) - continue - code = OK - valid_ans = True - except ValueError: - self.outfile.write(e_msg) - - return code, selection - - -# Display exit codes -OK = "ok" -"""Display exit code indicating user acceptance""" - -CANCEL = "cancel" -"""Display exit code for a user canceling the display""" - -HELP = "help" -"""Display exit code when for when the user requests more help.""" - - -def gen_https_names(domains): - """Returns a string of the https domains. - - Domains are formatted nicely with https:// prepended to each. - .. todo:: This should not use +=, rewrite this with unittests - - """ - result = "" - if len(domains) > 2: - for i in range(len(domains)-1): - result = result + "https://" + domains[i] + ", " - result = result + "and " - if len(domains) == 2: - return "https://" + domains[0] + " and https://" + domains[1] - if domains: - result = result + "https://" + domains[len(domains)-1] - - return result diff --git a/letsencrypt/client/display/display_util.py b/letsencrypt/client/display/display_util.py index 194350ca3..306729e40 100644 --- a/letsencrypt/client/display/display_util.py +++ b/letsencrypt/client/display/display_util.py @@ -5,6 +5,7 @@ import textwrap import dialog import zope.interface +from letsencrypt.client import errors from letsencrypt.client import interfaces @@ -49,8 +50,8 @@ class NcursesDisplay(object): :param str message: title of menu - :param choices: menu lines - :type choices: list of tuples (tag, item) or + :param choices: menu lines, len must be > 0 + :type choices: list of tuples (`tag`, `item`) tags must be unique or list of items (tags will be enumerated) :param str ok_label: label of the OK button @@ -58,7 +59,7 @@ class NcursesDisplay(object): :returns: tuple of the form (`code`, `tag`) where `code` - `str` display_util exit code - `tag` - `str` or `int` index corresponding to the item chosen + `tag` - `int` index corresponding to the item chosen :rtype: tuple """ @@ -75,7 +76,13 @@ class NcursesDisplay(object): help_button=help_button, help_label=help_label, width=self.width, height=self.height) - return code, str(selection) + # Return the selection index + for i, choice in enumerate(choices): + if choice[0] == selection: + return code, i + + return code, -1 + else: choices = [ (str(i), choice) for i, choice in enumerate(choices, 1) @@ -103,6 +110,8 @@ class NcursesDisplay(object): def yesno(self, message, yes_label="Yes", no_label="No"): """Display a Yes/No dialog box + Yes and No label must begin with different letters. + :param str message: message to display to user :param str yes_label: label on the "yes" button :param str no_label: label on the "no" button @@ -120,6 +129,7 @@ class NcursesDisplay(object): :param message: Message to display before choices :param list tags: where each is of type :class:`str` + len(tags) > 0 :returns: tuple of the form (code, list_tags) where `code` - int display exit code @@ -161,7 +171,7 @@ class FileDisplay(object): """Display a menu. :param str message: title of menu - :param choices: Menu lines + :param choices: Menu lines, len must be > 0 :type choices: list of tuples (tag, item) or list of descriptions (tags will be enumerated) @@ -175,7 +185,7 @@ class FileDisplay(object): code, selection = self._get_valid_int_ans(len(choices)) - return code, str(selection - 1) + return code, selection - 1 def input(self, message): # pylint: disable=no-self-use @@ -190,7 +200,7 @@ class FileDisplay(object): """ ans = raw_input( - textwrap.fill("%s (Enter c to cancel): " % message, 80)) + textwrap.fill("%s (Enter 'c' to cancel): " % message, 80)) if ans == "c" or ans == "C": return CANCEL, "-1" @@ -200,6 +210,8 @@ class FileDisplay(object): def yesno(self, message, yes_label="Yes", no_label="No"): """Query the user with a yes/no question. + Yes and No label must begin with different letters + :param str message: question for the user :param str yes_label: Label of the "Yes" parameter :param str no_label: Label of the "No" parameter @@ -215,10 +227,9 @@ class FileDisplay(object): self.outfile.write("{0}{frame}{msg}{0}{frame}".format( os.linesep, frame=side_frame, msg=message)) - yes_label = _parens_around_char(yes_label) - no_label = _parens_around_char(no_label) - - ans = raw_input("{yes}/{no}: ".format(yes=yes_label, no=no_label)) + ans = raw_input("{yes}/{no}: ".format( + yes=_parens_around_char(yes_label), + no=_parens_around_char(no_label))) return (ans.startswith(yes_label[0].lower()) or ans.startswith(yes_label[0].upper())) @@ -227,7 +238,7 @@ class FileDisplay(object): """Display a checklist. :param str message: Message to display to user - :param list tags: `str` tags to select + :param list tags: `str` tags to select, len(tags) > 0 :returns: tuple of (`code`, `tags`) where `code` - str display exit code @@ -255,7 +266,7 @@ class FileDisplay(object): def _scrub_checklist_input(self, indices, tags): """Validate input and transform indices to appropriate tags. - :param list indices: Checklist input + :param list indices: input :param list tags: Original tags of the checklist :returns: tags the user selected @@ -265,7 +276,7 @@ class FileDisplay(object): # They should all be of type int try: indices = [int(index) for index in indices] - except TypeError: + except ValueError: return [] # Remove duplicates @@ -292,14 +303,16 @@ class FileDisplay(object): if choices and isinstance(choices[0], tuple): choices = ["%s - %s" % (c[0], c[1]) for c in choices] + # Write out the message to the user self.outfile.write( "{new}{msg}{new}".format(new=os.linesep, msg=message)) side_frame = ("-" * 79) + os.linesep self.outfile.write(side_frame) - for i, tag in enumerate(choices, 1): + # Write out the menu choices + for i, desc in enumerate(choices, 1): self.outfile.write( - textwrap.fill("{num}: {tag}".format(num=i, tag=tag), 80)) + textwrap.fill("{num}: {desc}".format(num=i, desc=desc), 80)) # Keep this outside of the textwrap self.outfile.write(os.linesep) @@ -311,7 +324,7 @@ class FileDisplay(object): :param str msg: Original message - :returns: Formatted message + :returns: Formatted message respecting newlines in message :rtype: str """ @@ -325,7 +338,7 @@ class FileDisplay(object): def _get_valid_int_ans(self, max): """Get a numerical selection. - :param int max: The maximum entry (len of choices) + :param int max: The maximum entry (len of choices), must be positive :returns: tuple of the form (`code`, `selection`) where `code` - str display exit code ('ok' or cancel') @@ -337,10 +350,10 @@ class FileDisplay(object): if max > 1: input_msg = ("Select the appropriate number " "[1-{max}] then [enter] (press 'c' to " - "cancel){end}".format(max=max, end=os.linesep)) + "cancel): ".format(max=max)) else: input_msg = ("Press 1 [enter] to confirm the selection " - "(press 'c' to cancel){0}".format(os.linesep)) + "(press 'c' to cancel): ") while selection < 1: ans = raw_input(input_msg) if ans.startswith("c") or ans.startswith("C"): @@ -348,6 +361,7 @@ class FileDisplay(object): try: selection = int(ans) if selection < 1 or selection > max: + selection = -1 raise ValueError except ValueError: diff --git a/letsencrypt/client/enhance_display.py b/letsencrypt/client/enhance_display.py deleted file mode 100644 index 9de58127b..000000000 --- a/letsencrypt/client/enhance_display.py +++ /dev/null @@ -1,67 +0,0 @@ -"""Let's Encrypt Enhancement Display""" -import logging - -import zope.component - -from letsencrypt.client import errors -from letsencrypt.client import interfaces - - -class EnhanceDisplay(object): - """Class used to display various enhancements. - - .. note::This is not a subclass of Display. It merely uses Display as a - component. - - :ivar displayer: Display singleton - :type displayer: :class:`letsencrypt.client.interfaces.IDisplay - - :ivar dict dispatch: Dict mapping enhancements to functions - - """ - def __init__(self): - self.displayer = zope.component.getUtility(interfaces.IDisplay) - - self.dispatch = { - "redirect": self.redirect_by_default, - } - - def ask(self, enhancement): - """Display the enhancement to the user. - - :param str enhancement: One of the - :class:`letsencrypt.client.CONFIG.ENHANCEMENTS` enhancements - - :returns: True if feature is desired, False otherwise - :rtype: bool - - :raises :class:`letsencrypt.client.errors.LetsEncryptClientError`: If - the enhancement provided is not supported. - - """ - try: - return self.dispatch[enhancement] - except KeyError: - logging.error("Unsupported enhancement given to ask()") - raise errors.LetsEncryptClientError("Unsupported Enhancement") - - def redirect_by_default(self): - """Determines whether the user would like to redirect to HTTPS. - - :returns: True if redirect is desired, False otherwise - :rtype: bool - - """ - choices = [ - ("Easy", "Allow both HTTP and HTTPS access to these sites"), - ("Secure", "Make all requests redirect to secure HTTPS access")] - - result = self.displayer.menu( - "Please choose whether HTTPS access is required or optional.", - choices, "Please enter the appropriate number") - - if result[0] != OK: - return False - - # different answer for each type of display - return str(result[1]) == "Secure" or result[1] == 1 \ No newline at end of file diff --git a/letsencrypt/client/interfaces.py b/letsencrypt/client/interfaces.py index 757c1d705..3ceeee3e8 100644 --- a/letsencrypt/client/interfaces.py +++ b/letsencrypt/client/interfaces.py @@ -141,15 +141,22 @@ class IDisplay(zope.interface.Interface): """ - def menu(message, choices, input_text="", ok_label="OK", help_label=""): + def menu(message, choices, + ok_label="OK", cancel_label="Cancel", help_label=""): """Displays a generic menu. :param str message: message to display :param choices: choices - :type choices: :class:`list` of :func:`tuple` + :type choices: :class:`list` of :func:`tuple` or :class:`str` - :param str input_text: instructions on how to make a selection + :param str ok_label: label for OK button + :param str cancel_label: label for Cancel button + :param str help_label: label for Help button + + :returns: tuple of (`code`, `index`) where + `code` - str display exit code + `index` - int index of the user's selection """ @@ -168,6 +175,8 @@ class IDisplay(zope.interface.Interface): def yesno(message, yes_label="Yes", no_label="No"): """Query the user with a yes/no question. + Yes and No label must begin with different letters. + :param str message: question for the user :returns: True for "Yes", False for "No" @@ -175,13 +184,13 @@ class IDisplay(zope.interface.Interface): """ - def checkbox(message, choices): + def checklist(message, choices): """Allow for multiple selections from a menu. :param str message: message to display to the user - :param choices: :param choices: choices - :type choices: :class:`list` of :func:`tuple` + :param tags: tags + :type tags: :class:`list` of :class:`str` """ diff --git a/letsencrypt/client/tests/display/display_util_test.py b/letsencrypt/client/tests/display/display_util_test.py index 3c527fd87..3b6ccf83a 100644 --- a/letsencrypt/client/tests/display/display_util_test.py +++ b/letsencrypt/client/tests/display/display_util_test.py @@ -1,4 +1,5 @@ -import sys +import contextlib +import os import unittest import mock @@ -10,6 +11,7 @@ class DisplayT(unittest.TestCase): def setUp(self): self.choices = [("First", "Description1"), ("Second", "Description2")] self.tags = ["tag1", "tag2", "tag3"] + self.tags_choices = [("1", "tag1"), ("2", "tag2"), ("3", "tag3")] def test_visual(displayer, choices): @@ -21,13 +23,21 @@ def test_visual(displayer, choices): ok_label="O", cancel_label="Can", help_label="??") displayer.input("Input Message") displayer.yesno( - "Yes/No Message", yes_label="Yessir", no_label="Nosir") + "YesNo Message", yes_label="Yessir", no_label="Nosir") displayer.checklist( "Checklist Message", [choice[0] for choice in choices]) class NcursesDisplayTest(DisplayT): - """Test ncurses display.""" + """Test ncurses display. + + Since this is mostly a wrapper, it might be more helpful to test the actual + dialog boxes. The test_visual function will actually display the various + boxes but requires the user to do the verification. If something seems amiss + please use the test_visual function to debug it, the automatic tests rely + on too much mocking. + + """ def setUp(self): super(NcursesDisplayTest, self).setUp() self.displayer = display_util.NcursesDisplay() @@ -39,21 +49,233 @@ class NcursesDisplayTest(DisplayT): self.assertEqual(mock_msgbox.call_count, 1) @mock.patch("letsencrypt.client.display.display_util.dialog.Dialog.menu") - def test_menu(self, mock_menu): - pass + def test_menu_tag_and_desc(self, mock_menu): + mock_menu.return_value = (display_util.OK, "First") - def test_visual(self): - test_visual(self.displayer, self.choices) + ret = self.displayer.menu("Message", self.choices) + mock_menu.assert_called_with( + "Message", choices=self.choices, ok_label="OK", + cancel_label="Cancel", + help_button=False, help_label="", + width=display_util.WIDTH, height=display_util.HEIGHT) + + self.assertEqual(ret, (display_util.OK, 0)) + + @mock.patch("letsencrypt.client.display.display_util.dialog.Dialog.menu") + def test_menu_tag_and_desc_cancel(self, mock_menu): + mock_menu.return_value = (display_util.CANCEL, "") + + ret = self.displayer.menu("Message", self.choices) + + + mock_menu.assert_called_with( + "Message", choices=self.choices, ok_label="OK", + cancel_label="Cancel", + help_button=False, help_label="", + width=display_util.WIDTH, height=display_util.HEIGHT) + + self.assertEqual(ret, (display_util.CANCEL, -1)) + + @mock.patch("letsencrypt.client.display.display_util.dialog.Dialog.menu") + def test_menu_desc_only(self, mock_menu): + mock_menu.return_value = (display_util.OK, "1") + + ret = self.displayer.menu("Message", self.tags, help_label="More Info") + + + mock_menu.assert_called_with( + "Message", choices=self.tags_choices, ok_label="OK", + cancel_label="Cancel", + help_button=True, help_label="More Info", + width=display_util.WIDTH, height=display_util.HEIGHT) + + self.assertEqual(ret, (display_util.OK, 0)) + + @mock.patch("letsencrypt.client.display.display_util." + "dialog.Dialog.inputbox") + def test_input(self, mock_input): + self.displayer.input("message") + mock_input.assert_called_with("message") + + @mock.patch("letsencrypt.client.display.display_util.dialog.Dialog.yesno") + def test_yesno(self, mock_yesno): + mock_yesno.return_value = display_util.OK + + self.assertTrue(self.displayer.yesno("message")) + + mock_yesno.assert_called_with( + "message", display_util.HEIGHT, display_util.WIDTH, + yes_label="Yes", no_label="No") + + @mock.patch("letsencrypt.client.display.display_util." + "dialog.Dialog.checklist") + def test_checklist(self, mock_checklist): + self.displayer.checklist("message", self.tags) + + choices = [ + (self.tags[0], "", False), + (self.tags[1], "", False), + (self.tags[2], "", False) + ] + mock_checklist.assert_called_with( + "message", width=display_util.WIDTH, height=display_util.HEIGHT, + choices=choices) + + # def test_visual(self): + # test_visual(self.displayer, self.choices) class FileOutputDisplayTest(DisplayT): - """Test stdout display.""" + """Test stdout display. + + Most of this class has to deal with visual output. In order to test how the + functions look to a user, uncomment the test_visual function. + + """ def setUp(self): super(FileOutputDisplayTest, self).setUp() - self.displayer = display_util.FileDisplay(sys.stdout) + self.mock_stdout = mock.MagicMock() + self.displayer = display_util.FileDisplay(self.mock_stdout) - def test_visual(self): - test_visual(self.displayer, self.choices) + def test_notification_no_pause(self): + self.displayer.notification("message", 10, False) + string = self.mock_stdout.write.call_args[0][0] + + self.assertTrue("message" in string) + + def test_notification_pause(self): + # Attempt to mock raw_input + with mock_raw_input(["enter"]): + self.displayer.notification("message") + + self.assertTrue("message" in self.mock_stdout.write.call_args[0][0]) + + @mock.patch("letsencrypt.client.display.display_util." + "FileDisplay._get_valid_int_ans") + def test_menu(self, mock_ans): + mock_ans.return_value = (display_util.OK, 1) + ret = self.displayer.menu("message", self.choices) + self.assertEqual(ret, (display_util.OK, 0)) + + def test_input_cancel(self): + # Attempt to mock raw_input + with mock_raw_input(["c"]): + code, _ = self.displayer.input("message") + + self.assertTrue(code, display_util.CANCEL) + + def test_input_normal(self): + with mock_raw_input(["domain.com"]): + code, input_ = self.displayer.input("message") + + self.assertEqual(code, display_util.OK) + self.assertEqual(input_, "domain.com") + + def test_yesno(self): + with mock_raw_input(["Yes"]): + self.assertTrue(self.displayer.yesno("message")) + with mock_raw_input(["y"]): + self.assertTrue(self.displayer.yesno("message")) + with mock_raw_input(["cancel"]): + self.assertFalse(self.displayer.yesno("message")) + + with mock_raw_input(["a"]): + self.assertTrue(self.displayer.yesno("msg", yes_label="Agree")) + + @mock.patch("letsencrypt.client.display.display_util.FileDisplay.input") + def test_checklist_valid(self, mock_input): + mock_input.return_value = (display_util.OK, "2 1") + code, tag_list = self.displayer.checklist("msg", self.tags) + self.assertEqual( + (code, set(tag_list)), (display_util.OK, set(["tag1", "tag2"]))) + + @mock.patch("letsencrypt.client.display.display_util.FileDisplay.input") + def test_checklist_miss_valid(self, mock_input): + mock_input.side_effect = [ + (display_util.OK, "10"), + (display_util.OK, "tag1 please"), + (display_util.OK, "1") + ] + + ret = self.displayer.checklist("msg", self.tags) + self.assertEqual(ret, (display_util.OK, ["tag1"])) + + @mock.patch("letsencrypt.client.display.display_util.FileDisplay.input") + def test_checklist_miss_quit(self, mock_input): + mock_input.side_effect = [ + (display_util.OK, "10"), + (display_util.CANCEL, "1") + ] + ret = self.displayer.checklist("msg", self.tags) + self.assertEqual(ret, (display_util.CANCEL, [])) + + def test_scrub_checklist_input_valid(self): + indices = [ + ["1"], + ["1", "2", "1"], + ["2", "3"], + ] + exp = [ + set(["tag1"]), + set(["tag1", "tag2"]), + set(["tag2", "tag3"]), + ] + for i, list_ in enumerate(indices): + set_tags = set( + self.displayer._scrub_checklist_input(list_, self.tags)) + self.assertEqual(set_tags, exp[i]) + + def test_scrub_checklist_input_invalid(self): + indices = [ + ["0"], + ["4"], + ["tag1"], + ["1", "tag1"], + ["2", "o"] + ] + for list_ in indices: + self.assertEqual( + self.displayer._scrub_checklist_input(list_, self.tags), []) + + def test_print_menu(self): + # This is purely cosmetic... just make sure there aren't any exceptions + self.displayer._print_menu("msg", self.choices) + self.displayer._print_menu("msg", self.tags) + + def test_wrap_lines(self): + msg = ("This is just a weak test\n" + "This function is only meant to be for easy viewing\n" + "Test a really really really really really really really really " + "really really really really really long line...") + text = self.displayer._wrap_lines(msg) + + self.assertEqual(text.count(os.linesep), 3) + + def test_get_valid_int_ans_valid(self): + with mock_raw_input(["1"]): + self.assertEqual( + self.displayer._get_valid_int_ans(1), (display_util.OK, 1)) + ans = "2" + with mock_raw_input([ans]): + self.assertEqual( + self.displayer._get_valid_int_ans(3), + (display_util.OK, int(ans))) + + def test_get_valid_int_ans_invalid(self): + answers = [ + ["0", "c"], + ["4", "one", "C"], + ["c"], + ] + for ans in answers: + with mock_raw_input(ans): + self.assertEqual( + self.displayer._get_valid_int_ans(3), + (display_util.CANCEL, -1)) + + # def test_visual(self): + # self.displayer = display_util.FileDisplay(sys.stdout) + # test_visual(self.displayer, self.choices) class SeparateListInputTest(unittest.TestCase): @@ -101,5 +323,15 @@ class PlaceParensTest(unittest.TestCase): self.assertEqual("(L)abel", ret) +# https://stackoverflow.com/a/25275926 +@contextlib.contextmanager +def mock_raw_input(values): + func = mock.MagicMock(side_effect=values) + original_raw_input = __builtins__.raw_input + __builtins__.raw_input = func + yield + __builtins__.raw_input = original_raw_input + + if __name__ == "__main__": unittest.main() \ No newline at end of file