diff --git a/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/failing/ipv6-1143.conf b/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/failing/ipv6-1143.conf
deleted file mode 100644
index ab4ed412e..000000000
--- a/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/failing/ipv6-1143.conf
+++ /dev/null
@@ -1,9 +0,0 @@
-
-DocumentRoot /xxxx/
-ServerName noodles.net.nz
-ServerAlias www.noodles.net.nz
-CustomLog ${APACHE_LOG_DIR}/domlogs/noodles.log combined
-
- AllowOverride All
-
-
diff --git a/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/failing/ipv6-1143b.conf b/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/failing/ipv6-1143b.conf
deleted file mode 100644
index 25655a07c..000000000
--- a/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/failing/ipv6-1143b.conf
+++ /dev/null
@@ -1,21 +0,0 @@
-
-
-DocumentRoot /xxxx/
-ServerName noodles.net.nz
-ServerAlias www.noodles.net.nz
-CustomLog ${APACHE_LOG_DIR}/domlogs/noodles.log combined
-
- AllowOverride All
-
-
- SSLEngine on
-
- SSLHonorCipherOrder On
- SSLProtocol all -SSLv2 -SSLv3
- SSLCipherSuite "EECDH+ECDSA+AESGCM EECDH+aRSA+AESGCM EECDH+ECDSA+SHA384 EECDH+ECDSA+SHA256 EECDH+aRSA+SHA384 EECDH+aRSA+SHA256 EECDH+aRSA+RC4 EECDH EDH +aRSA RC4 !aNULL !eNULL !LOW !3DES !MD5 !EXP !PSK !SRP !DSS"
-
- SSLCertificateFile /xxxx/noodles.net.nz.crt
- SSLCertificateKeyFile /xxxx/noodles.net.nz.key
-
- Header set Strict-Transport-Security "max-age=31536000; preload"
-
diff --git a/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143.conf b/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143.conf
new file mode 100644
index 000000000..ad988dc05
--- /dev/null
+++ b/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143.conf
@@ -0,0 +1,9 @@
+
+DocumentRoot /tmp
+ServerName example.com
+ServerAlias www.example.com
+CustomLog ${APACHE_LOG_DIR}/example.log combined
+
+ AllowOverride All
+
+
diff --git a/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143b.conf b/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143b.conf
new file mode 100644
index 000000000..e2b4fd3da
--- /dev/null
+++ b/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143b.conf
@@ -0,0 +1,18 @@
+
+DocumentRoot /tmp
+ServerName example.com
+ServerAlias www.example.com
+CustomLog ${APACHE_LOG_DIR}/example.log combined
+
+ AllowOverride All
+
+
+ SSLEngine on
+
+ SSLHonorCipherOrder On
+ SSLProtocol all -SSLv2 -SSLv3
+ SSLCipherSuite "EECDH+ECDSA+AESGCM EECDH+aRSA+AESGCM EECDH+ECDSA+SHA384 EECDH+ECDSA+SHA256 EECDH+aRSA+SHA384 EECDH+aRSA+SHA256 EECDH+aRSA+RC4 EECDH EDH +aRSA RC4 !aNULL !eNULL !LOW !3DES !MD5 !EXP !PSK !SRP !DSS"
+
+ SSLCertificateFile /etc/ssl/certs/ssl-cert-snakeoil.pem
+ SSLCertificateKeyFile /etc/ssl/private/ssl-cert-snakeoil.key
+
diff --git a/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143c.conf b/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143c.conf
new file mode 100644
index 000000000..f2d2ecbea
--- /dev/null
+++ b/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143c.conf
@@ -0,0 +1,9 @@
+
+DocumentRoot /tmp
+ServerName example.com
+ServerAlias www.example.com
+CustomLog ${APACHE_LOG_DIR}/example.log combined
+
+ AllowOverride All
+
+
diff --git a/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143d.conf b/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143d.conf
new file mode 100644
index 000000000..f5b7a2b45
--- /dev/null
+++ b/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143d.conf
@@ -0,0 +1,18 @@
+
+DocumentRoot /tmp
+ServerName example.com
+ServerAlias www.example.com
+CustomLog ${APACHE_LOG_DIR}/example.log combined
+
+ AllowOverride All
+
+
+ SSLEngine on
+
+ SSLHonorCipherOrder On
+ SSLProtocol all -SSLv2 -SSLv3
+ SSLCipherSuite "EECDH+ECDSA+AESGCM EECDH+aRSA+AESGCM EECDH+ECDSA+SHA384 EECDH+ECDSA+SHA256 EECDH+aRSA+SHA384 EECDH+aRSA+SHA256 EECDH+aRSA+RC4 EECDH EDH +aRSA RC4 !aNULL !eNULL !LOW !3DES !MD5 !EXP !PSK !SRP !DSS"
+
+ SSLCertificateFile /etc/ssl/certs/ssl-cert-snakeoil.pem
+ SSLCertificateKeyFile /etc/ssl/private/ssl-cert-snakeoil.key
+
diff --git a/letsencrypt-apache/letsencrypt_apache/tests/testdata/debian_apache_2_4/multiple_vhosts/apache2/sites-available/000-default.conf b/letsencrypt-apache/letsencrypt_apache/tests/testdata/debian_apache_2_4/multiple_vhosts/apache2/sites-available/000-default.conf
index c759768c5..2bd4e1fe9 100644
--- a/letsencrypt-apache/letsencrypt_apache/tests/testdata/debian_apache_2_4/multiple_vhosts/apache2/sites-available/000-default.conf
+++ b/letsencrypt-apache/letsencrypt_apache/tests/testdata/debian_apache_2_4/multiple_vhosts/apache2/sites-available/000-default.conf
@@ -1,4 +1,4 @@
-
+
ServerName ip-172-30-0-17
ServerAdmin webmaster@localhost
diff --git a/letsencrypt-apache/letsencrypt_apache/tests/util.py b/letsencrypt-apache/letsencrypt_apache/tests/util.py
index 928084e3c..2fbfd70c6 100644
--- a/letsencrypt-apache/letsencrypt_apache/tests/util.py
+++ b/letsencrypt-apache/letsencrypt_apache/tests/util.py
@@ -133,8 +133,9 @@ def get_vh_truth(temp_dir, config_name):
obj.VirtualHost(
os.path.join(prefix, "000-default.conf"),
os.path.join(aug_pre, "000-default.conf/VirtualHost"),
- set([obj.Addr.fromstring("*:80")]), False, True,
- "ip-172-30-0-17"),
+ set([obj.Addr.fromstring("*:80"),
+ obj.Addr.fromstring("[::]:80")]),
+ False, True, "ip-172-30-0-17"),
obj.VirtualHost(
os.path.join(prefix, "letsencrypt.conf"),
os.path.join(aug_pre, "letsencrypt.conf/VirtualHost"),
diff --git a/letsencrypt/display/completer.py b/letsencrypt/display/completer.py
new file mode 100644
index 000000000..fed476bb3
--- /dev/null
+++ b/letsencrypt/display/completer.py
@@ -0,0 +1,61 @@
+"""Provides Tab completion when prompting users for a path."""
+import glob
+# readline module is not available on all systems
+try:
+ import readline
+except ImportError:
+ import letsencrypt.display.dummy_readline as readline
+
+
+class Completer(object):
+ """Provides Tab completion when prompting users for a path.
+
+ This class is meant to be used with readline to provide Tab
+ completion for users entering paths. The complete method can be
+ passed to readline.set_completer directly, however, this function
+ works best as a context manager. For example:
+
+ with Completer():
+ raw_input()
+
+ In this example, Tab completion will be available during the call to
+ raw_input above, however, readline will be restored to its previous
+ state when exiting the body of the with statement.
+
+ """
+
+ def __init__(self):
+ self._iter = self._original_completer = self._original_delims = None
+
+ def complete(self, text, state):
+ """Provides path completion for use with readline.
+
+ :param str text: text to offer completions for
+ :param int state: which completion to return
+
+ :returns: possible completion for text or ``None`` if all
+ completions have been returned
+ :rtype: str
+
+ """
+ if state == 0:
+ self._iter = glob.iglob(text + '*')
+ return next(self._iter, None)
+
+ def __enter__(self):
+ self._original_completer = readline.get_completer()
+ self._original_delims = readline.get_completer_delims()
+
+ readline.set_completer(self.complete)
+ readline.set_completer_delims(' \t\n;')
+
+ # readline can be implemented using GNU readline or libedit
+ # which have different configuration syntax
+ if 'libedit' in readline.__doc__:
+ readline.parse_and_bind('bind ^I rl_complete')
+ else:
+ readline.parse_and_bind('tab: complete')
+
+ def __exit__(self, unused_type, unused_value, unused_traceback):
+ readline.set_completer_delims(self._original_delims)
+ readline.set_completer(self._original_completer)
diff --git a/letsencrypt/display/dummy_readline.py b/letsencrypt/display/dummy_readline.py
new file mode 100644
index 000000000..fb3d807bb
--- /dev/null
+++ b/letsencrypt/display/dummy_readline.py
@@ -0,0 +1,21 @@
+"""A dummy module with no effect for use on systems without readline."""
+
+
+def get_completer():
+ """An empty implementation of readline.get_completer."""
+
+
+def get_completer_delims():
+ """An empty implementation of readline.get_completer_delims."""
+
+
+def parse_and_bind(unused_command):
+ """An empty implementation of readline.parse_and_bind."""
+
+
+def set_completer(unused_function=None):
+ """An empty implementation of readline.set_completer."""
+
+
+def set_completer_delims(unused_delims):
+ """An empty implementation of readline.set_completer_delims."""
diff --git a/letsencrypt/display/util.py b/letsencrypt/display/util.py
index 84049c47c..20c6be156 100644
--- a/letsencrypt/display/util.py
+++ b/letsencrypt/display/util.py
@@ -7,10 +7,18 @@ import zope.interface
from letsencrypt import interfaces
from letsencrypt import errors
+from letsencrypt.display import completer
WIDTH = 72
HEIGHT = 20
+DSELECT_HELP = (
+ "Use the arrow keys or Tab to move between window elements. Space can be "
+ "used to complete the input path with the selected element in the "
+ "directory window. Pressing enter will select the currently highlighted "
+ "button.")
+"""Help text on how to use dialog's dselect."""
+
# Display exit codes
OK = "ok"
"""Display exit code indicating user acceptance."""
@@ -21,6 +29,7 @@ CANCEL = "cancel"
HELP = "help"
"""Display exit code when for when the user requests more help."""
+
def _wrap_lines(msg):
"""Format lines nicely to 80 chars.
@@ -36,6 +45,7 @@ def _wrap_lines(msg):
fixed_l.append(textwrap.fill(line, 80))
return os.linesep.join(fixed_l)
+
@zope.interface.implementer(interfaces.IDisplay)
class NcursesDisplay(object):
"""Ncurses-based display."""
@@ -118,7 +128,6 @@ class NcursesDisplay(object):
return code, int(index) - 1
-
def input(self, message, **unused_kwargs):
"""Display an input box to the user.
@@ -132,11 +141,10 @@ class NcursesDisplay(object):
"""
sections = message.split("\n")
# each section takes at least one line, plus extras if it's longer than self.width
- wordlines = [1 + (len(section)/self.width) for section in sections]
+ wordlines = [1 + (len(section) / self.width) for section in sections]
height = 6 + sum(wordlines) + len(sections)
return self.dialog.inputbox(message, width=self.width, height=height)
-
def yesno(self, message, yes_label="Yes", no_label="No", **unused_kwargs):
"""Display a Yes/No dialog box.
@@ -174,6 +182,21 @@ class NcursesDisplay(object):
return self.dialog.checklist(
message, width=self.width, height=self.height, choices=choices)
+ def directory_select(self, message, **unused_kwargs):
+ """Display a directory selection screen.
+
+ :param str message: prompt to give the user
+
+ :returns: tuple of the form (`code`, `string`) where
+ `code` - int display exit code
+ `string` - input entered by the user
+
+ """
+ root_directory = os.path.abspath(os.sep)
+ return self.dialog.dselect(
+ filepath=root_directory, width=self.width,
+ height=self.height, help_button=True, title=message)
+
@zope.interface.implementer(interfaces.IDisplay)
class FileDisplay(object):
@@ -317,6 +340,19 @@ class FileDisplay(object):
else:
return code, []
+ def directory_select(self, message, **unused_kwargs):
+ """Display a directory selection screen.
+
+ :param str message: prompt to give the user
+
+ :returns: tuple of the form (`code`, `string`) where
+ `code` - int display exit code
+ `string` - input entered by the user
+
+ """
+ with completer.Completer():
+ return self.input(message)
+
def _scrub_checklist_input(self, indices, tags):
# pylint: disable=no-self-use
"""Validate input and transform indices to appropriate tags.
@@ -373,7 +409,6 @@ class FileDisplay(object):
self.outfile.write(side_frame)
-
def _get_valid_int_ans(self, max_):
"""Get a numerical selection.
@@ -409,6 +444,7 @@ class FileDisplay(object):
return OK, selection
+
@zope.interface.implementer(interfaces.IDisplay)
class NoninteractiveDisplay(object):
"""An iDisplay implementation that never asks for interactive user input"""
@@ -483,7 +519,6 @@ class NoninteractiveDisplay(object):
else:
return OK, default
-
def yesno(self, message, yes_label=None, no_label=None, default=None, cli_flag=None):
# pylint: disable=unused-argument
"""Decide Yes or No, without asking anybody
@@ -520,6 +555,25 @@ class NoninteractiveDisplay(object):
else:
return OK, default
+ def directory_select(self, message, default=None, cli_flag=None):
+ """Simulate prompting the user for a directory.
+
+ This function returns default if it is not ``None``, otherwise,
+ an exception is raised explaining the problem. If cli_flag is
+ not ``None``, the error message will include the flag that can
+ be used to set this value with the CLI.
+
+ :param str message: prompt to give the user
+ :param default: default value to return (if one exists)
+ :param str cli_flag: option used to set this value with the CLI
+
+ :returns: tuple of the form (`code`, `string`) where
+ `code` - int display exit code
+ `string` - input entered by the user
+
+ """
+ return self.input(message, default, cli_flag)
+
def separate_list_input(input_):
"""Separate a comma or space separated list.
diff --git a/letsencrypt/interfaces.py b/letsencrypt/interfaces.py
index 1921b1e54..2fba11869 100644
--- a/letsencrypt/interfaces.py
+++ b/letsencrypt/interfaces.py
@@ -443,6 +443,22 @@ class IDisplay(zope.interface.Interface):
"""
+ def directory_select(self, message, default=None, cli_flag=None):
+ """Display a directory selection screen.
+
+ :param str message: prompt to give the user
+ :param default: the default value to return, if one exists, when
+ using the NoninteractiveDisplay
+ :param str cli_flag: option used to set this value with the CLI,
+ if one exists, to be included in error messages given by
+ NoninteractiveDisplay
+
+ :returns: tuple of the form (`code`, `string`) where
+ `code` - int display exit code
+ `string` - input entered by the user
+
+ """
+
class IValidator(zope.interface.Interface):
"""Configuration validator."""
diff --git a/letsencrypt/plugins/common.py b/letsencrypt/plugins/common.py
index f6a2c3d76..a9410d514 100644
--- a/letsencrypt/plugins/common.py
+++ b/letsencrypt/plugins/common.py
@@ -104,14 +104,24 @@ class Addr(object):
:param str port: port number or \*, or ""
"""
- def __init__(self, tup):
+ def __init__(self, tup, ipv6=False):
self.tup = tup
+ self.ipv6 = ipv6
@classmethod
def fromstring(cls, str_addr):
"""Initialize Addr from string."""
- tup = str_addr.partition(':')
- return cls((tup[0], tup[2]))
+ if str_addr.startswith('['):
+ # ipv6 addresses starts with [
+ endIndex = str_addr.rfind(']')
+ host = str_addr[:endIndex + 1]
+ port = ''
+ if len(str_addr) > endIndex + 2 and str_addr[endIndex + 1] == ':':
+ port = str_addr[endIndex + 2:]
+ return cls((host, port), ipv6=True)
+ else:
+ tup = str_addr.partition(':')
+ return cls((tup[0], tup[2]))
def __str__(self):
if self.tup[1]:
@@ -120,7 +130,16 @@ class Addr(object):
def __eq__(self, other):
if isinstance(other, self.__class__):
- return self.tup == other.tup
+ if self.ipv6:
+ # compare normalized to take different
+ # styles of representation into account
+ return (other.ipv6 and
+ self._normalize_ipv6(self.tup[0]) ==
+ self._normalize_ipv6(other.tup[0]) and
+ self.tup[1] == other.tup[1])
+ else:
+ return self.tup == other.tup
+
return False
def __hash__(self):
@@ -136,7 +155,44 @@ class Addr(object):
def get_addr_obj(self, port):
"""Return new address object with same addr and new port."""
- return self.__class__((self.tup[0], port))
+ return self.__class__((self.tup[0], port), self.ipv6)
+
+ def _normalize_ipv6(self, addr):
+ """Return IPv6 address in normalized form, helper function"""
+ addr = addr.lstrip("[")
+ addr = addr.rstrip("]")
+ return self._explode_ipv6(addr)
+
+ def get_ipv6_exploded(self):
+ """Return IPv6 in normalized form"""
+ if self.ipv6:
+ return ":".join(self._normalize_ipv6(self.tup[0]))
+ return ""
+
+ def _explode_ipv6(self, addr):
+ """Explode IPv6 address for comparison"""
+ result = ['0', '0', '0', '0', '0', '0', '0', '0']
+ addr_list = addr.split(":")
+ if len(addr_list) > len(result):
+ # too long, truncate
+ addr_list = addr_list[0:len(result)]
+ append_to_end = False
+ for i in range(0, len(addr_list)):
+ block = addr_list[i]
+ if len(block) == 0:
+ # encountered ::, so rest of the blocks should be
+ # appended to the end
+ append_to_end = True
+ continue
+ elif len(block) > 1:
+ # remove leading zeros
+ block = block.lstrip("0")
+ if not append_to_end:
+ result[i] = str(block)
+ else:
+ # count the location from the end using negative indices
+ result[i-len(addr_list)] = str(block)
+ return result
class TLSSNI01(object):
diff --git a/letsencrypt/plugins/common_test.py b/letsencrypt/plugins/common_test.py
index 55319f0a0..a4292151e 100644
--- a/letsencrypt/plugins/common_test.py
+++ b/letsencrypt/plugins/common_test.py
@@ -81,6 +81,11 @@ class AddrTest(unittest.TestCase):
self.addr1 = Addr.fromstring("192.168.1.1")
self.addr2 = Addr.fromstring("192.168.1.1:*")
self.addr3 = Addr.fromstring("192.168.1.1:80")
+ self.addr4 = Addr.fromstring("[fe00::1]")
+ self.addr5 = Addr.fromstring("[fe00::1]:*")
+ self.addr6 = Addr.fromstring("[fe00::1]:80")
+ self.addr7 = Addr.fromstring("[fe00::1]:5")
+ self.addr8 = Addr.fromstring("[fe00:1:2:3:4:5:6:7:8:9]:8080")
def test_fromstring(self):
self.assertEqual(self.addr1.get_addr(), "192.168.1.1")
@@ -89,22 +94,49 @@ class AddrTest(unittest.TestCase):
self.assertEqual(self.addr2.get_port(), "*")
self.assertEqual(self.addr3.get_addr(), "192.168.1.1")
self.assertEqual(self.addr3.get_port(), "80")
+ self.assertEqual(self.addr4.get_addr(), "[fe00::1]")
+ self.assertEqual(self.addr4.get_port(), "")
+ self.assertEqual(self.addr5.get_addr(), "[fe00::1]")
+ self.assertEqual(self.addr5.get_port(), "*")
+ self.assertEqual(self.addr6.get_addr(), "[fe00::1]")
+ self.assertEqual(self.addr6.get_port(), "80")
+ self.assertEqual(self.addr6.get_ipv6_exploded(),
+ "fe00:0:0:0:0:0:0:1")
+ self.assertEqual(self.addr1.get_ipv6_exploded(),
+ "")
+ self.assertEqual(self.addr7.get_port(), "5")
+ self.assertEqual(self.addr8.get_ipv6_exploded(),
+ "fe00:1:2:3:4:5:6:7")
def test_str(self):
self.assertEqual(str(self.addr1), "192.168.1.1")
self.assertEqual(str(self.addr2), "192.168.1.1:*")
self.assertEqual(str(self.addr3), "192.168.1.1:80")
+ self.assertEqual(str(self.addr4), "[fe00::1]")
+ self.assertEqual(str(self.addr5), "[fe00::1]:*")
+ self.assertEqual(str(self.addr6), "[fe00::1]:80")
def test_get_addr_obj(self):
self.assertEqual(str(self.addr1.get_addr_obj("443")), "192.168.1.1:443")
self.assertEqual(str(self.addr2.get_addr_obj("")), "192.168.1.1")
self.assertEqual(str(self.addr1.get_addr_obj("*")), "192.168.1.1:*")
+ self.assertEqual(str(self.addr4.get_addr_obj("443")), "[fe00::1]:443")
+ self.assertEqual(str(self.addr5.get_addr_obj("")), "[fe00::1]")
+ self.assertEqual(str(self.addr4.get_addr_obj("*")), "[fe00::1]:*")
def test_eq(self):
self.assertEqual(self.addr1, self.addr2.get_addr_obj(""))
self.assertNotEqual(self.addr1, self.addr2)
self.assertFalse(self.addr1 == 3333)
+ self.assertEqual(self.addr4, self.addr4.get_addr_obj(""))
+ self.assertNotEqual(self.addr4, self.addr5)
+ self.assertFalse(self.addr4 == 3333)
+ from letsencrypt.plugins.common import Addr
+ self.assertEqual(self.addr4, Addr.fromstring("[fe00:0:0::1]"))
+ self.assertEqual(self.addr4, Addr.fromstring("[fe00:0::0:0:1]"))
+
+
def test_set_inclusion(self):
from letsencrypt.plugins.common import Addr
set_a = set([self.addr1, self.addr2])
@@ -114,6 +146,13 @@ class AddrTest(unittest.TestCase):
self.assertEqual(set_a, set_b)
+ set_c = set([self.addr4, self.addr5])
+ addr4b = Addr.fromstring("[fe00::1]")
+ addr5b = Addr.fromstring("[fe00::1]:*")
+ set_d = set([addr4b, addr5b])
+
+ self.assertEqual(set_c, set_d)
+
class TLSSNI01Test(unittest.TestCase):
"""Tests for letsencrypt.plugins.common.TLSSNI01."""
diff --git a/letsencrypt/tests/display/completer_test.py b/letsencrypt/tests/display/completer_test.py
new file mode 100644
index 000000000..3c181c925
--- /dev/null
+++ b/letsencrypt/tests/display/completer_test.py
@@ -0,0 +1,102 @@
+"""Test letsencrypt.display.completer."""
+import os
+import readline
+import shutil
+import string
+import sys
+import tempfile
+import unittest
+
+import mock
+from six.moves import reload_module # pylint: disable=import-error
+
+
+class CompleterTest(unittest.TestCase):
+ """Test letsencrypt.display.completer.Completer."""
+
+ def setUp(self):
+ self.temp_dir = tempfile.mkdtemp()
+
+ # directories must end with os.sep for completer to
+ # search inside the directory for possible completions
+ if self.temp_dir[-1] != os.sep:
+ self.temp_dir += os.sep
+
+ self.paths = []
+ # create some files and directories in temp_dir
+ for c in string.ascii_lowercase:
+ path = os.path.join(self.temp_dir, c)
+ self.paths.append(path)
+ if ord(c) % 2:
+ os.mkdir(path)
+ else:
+ with open(path, 'w'):
+ pass
+
+ def tearDown(self):
+ shutil.rmtree(self.temp_dir)
+
+ def test_complete(self):
+ from letsencrypt.display import completer
+ my_completer = completer.Completer()
+ num_paths = len(self.paths)
+
+ for i in range(num_paths):
+ completion = my_completer.complete(self.temp_dir, i)
+ self.assertTrue(completion in self.paths)
+ self.paths.remove(completion)
+
+ self.assertFalse(self.paths)
+ completion = my_completer.complete(self.temp_dir, num_paths)
+ self.assertEqual(completion, None)
+
+ def test_import_error(self):
+ original_readline = sys.modules['readline']
+ sys.modules['readline'] = None
+
+ self.test_context_manager_with_unmocked_readline()
+
+ sys.modules['readline'] = original_readline
+
+ def test_context_manager_with_unmocked_readline(self):
+ from letsencrypt.display import completer
+ reload_module(completer)
+
+ original_completer = readline.get_completer()
+ original_delims = readline.get_completer_delims()
+
+ with completer.Completer():
+ pass
+
+ self.assertEqual(readline.get_completer(), original_completer)
+ self.assertEqual(readline.get_completer_delims(), original_delims)
+
+ @mock.patch('letsencrypt.display.completer.readline', autospec=True)
+ def test_context_manager_libedit(self, mock_readline):
+ mock_readline.__doc__ = 'libedit'
+ self._test_context_manager_with_mock_readline(mock_readline)
+
+ @mock.patch('letsencrypt.display.completer.readline', autospec=True)
+ def test_context_manager_readline(self, mock_readline):
+ mock_readline.__doc__ = 'GNU readline'
+ self._test_context_manager_with_mock_readline(mock_readline)
+
+ def _test_context_manager_with_mock_readline(self, mock_readline):
+ from letsencrypt.display import completer
+
+ mock_readline.parse_and_bind.side_effect = enable_tab_completion
+
+ with completer.Completer():
+ pass
+
+ self.assertTrue(mock_readline.parse_and_bind.called)
+
+
+def enable_tab_completion(unused_command):
+ """Enables readline tab completion using the system specific syntax."""
+ libedit = 'libedit' in readline.__doc__
+ command = 'bind ^I rl_complete' if libedit else 'tab: complete'
+ readline.parse_and_bind(command)
+
+if __name__ == "__main__":
+ unittest.main() # pragma: no cover
diff --git a/letsencrypt/tests/display/util_test.py b/letsencrypt/tests/display/util_test.py
index a16eb544e..bae0d582a 100644
--- a/letsencrypt/tests/display/util_test.py
+++ b/letsencrypt/tests/display/util_test.py
@@ -123,6 +123,11 @@ class NcursesDisplayTest(unittest.TestCase):
"message", width=display_util.WIDTH, height=display_util.HEIGHT,
choices=choices)
+ @mock.patch("letsencrypt.display.util.dialog.Dialog.dselect")
+ def test_directory_select(self, mock_dselect):
+ self.displayer.directory_select("message")
+ self.assertEqual(mock_dselect.call_count, 1)
+
class FileOutputDisplayTest(unittest.TestCase):
"""Test stdout display.
@@ -227,6 +232,15 @@ class FileOutputDisplayTest(unittest.TestCase):
self.displayer._scrub_checklist_input(list_, TAGS))
self.assertEqual(set_tags, exp[i])
+ @mock.patch("letsencrypt.display.util.FileDisplay.input")
+ def test_directory_select(self, mock_input):
+ message = "msg"
+ result = (display_util.OK, "/var/www/html",)
+ mock_input.return_value = result
+
+ self.assertEqual(self.displayer.directory_select(message), result)
+ mock_input.assert_called_once_with(message)
+
def test_scrub_checklist_input_invalid(self):
# pylint: disable=protected-access
indices = [
@@ -280,6 +294,7 @@ class FileOutputDisplayTest(unittest.TestCase):
self.displayer._get_valid_int_ans(3),
(display_util.CANCEL, -1))
+
class NoninteractiveDisplayTest(unittest.TestCase):
"""Test non-interactive display.
@@ -320,6 +335,15 @@ class NoninteractiveDisplayTest(unittest.TestCase):
self.assertEqual(ret, (display_util.OK, d))
self.assertRaises(errors.MissingCommandlineFlag, self.displayer.checklist, "message", TAGS)
+ def test_directory_select(self):
+ default = "/var/www/html"
+ expected = (display_util.OK, default)
+ actual = self.displayer.directory_select("msg", default)
+ self.assertEqual(expected, actual)
+
+ self.assertRaises(
+ errors.MissingCommandlineFlag, self.displayer.directory_select, "msg")
+
class SeparateListInputTest(unittest.TestCase):
"""Test Module functions."""