2019-11-25 17:30:24 -05:00
|
|
|
"""Tests for certbot_nginx._internal.parser."""
|
2015-04-07 19:22:34 -04:00
|
|
|
import glob
|
2015-04-14 01:57:06 -04:00
|
|
|
import re
|
2015-04-07 17:57:37 -04:00
|
|
|
import shutil
|
|
|
|
|
import unittest
|
2021-03-23 16:33:47 -04:00
|
|
|
from typing import List
|
2015-04-07 17:57:37 -04:00
|
|
|
|
2016-04-13 19:45:54 -04:00
|
|
|
from certbot import errors
|
2019-04-12 16:32:52 -04:00
|
|
|
from certbot.compat import os
|
2019-11-25 17:30:24 -05:00
|
|
|
from certbot_nginx._internal import nginxparser
|
|
|
|
|
from certbot_nginx._internal import obj
|
|
|
|
|
from certbot_nginx._internal import parser
|
2019-11-26 20:45:18 -05:00
|
|
|
import test_util as util
|
2015-04-07 17:57:37 -04:00
|
|
|
|
|
|
|
|
|
2019-11-14 17:26:01 -05:00
|
|
|
class NginxParserTest(util.NginxTest):
|
2015-04-07 17:57:37 -04:00
|
|
|
"""Nginx Parser Test."""
|
|
|
|
|
|
|
|
|
|
def tearDown(self):
|
|
|
|
|
shutil.rmtree(self.temp_dir)
|
|
|
|
|
shutil.rmtree(self.config_dir)
|
|
|
|
|
shutil.rmtree(self.work_dir)
|
|
|
|
|
|
|
|
|
|
def test_root_normalized(self):
|
2015-05-04 11:33:53 -04:00
|
|
|
path = os.path.join(self.temp_dir, "etc_nginx/////"
|
|
|
|
|
"ubuntu_nginx/../../etc_nginx")
|
2017-05-02 20:56:56 -04:00
|
|
|
nparser = parser.NginxParser(path)
|
2015-04-18 13:20:19 -04:00
|
|
|
self.assertEqual(nparser.root, self.config_path)
|
2015-04-07 17:57:37 -04:00
|
|
|
|
|
|
|
|
def test_root_absolute(self):
|
Ensure relpath is executed on paths in the same drive (#7335)
On Windows you can have several drives (`C:`, `D:`, ...), that is the roughly (really roughly) equivalent of mount points, since each drive is usually associated to a specific physical partition.
So you can have paths like `C:\one\path`, `D:\another\path`.
In parallel, `os.path.relpath(path, start='.')` calculates the relative path between the given `path` and a `start` path (current directory if not provided). In recent versions of Python, `os.path.relpath` will fail if `path` and `start` are not on the same drive, because a relative path between two paths like `C:\one\path`, `D:\another\path` is not possible.
In saw unit tests failing because of this in two locations. This occurs when the certbot codebase that is tested is on a given drive (like `D:`) while the default temporary directory used by `tempfile` is on another drive (most of the time located in `C:` drive).
This PR fixes that.
2019-08-23 15:53:31 -04:00
|
|
|
curr_dir = os.getcwd()
|
|
|
|
|
try:
|
|
|
|
|
# On Windows current directory may be on a different drive than self.tempdir.
|
|
|
|
|
# However a relative path between two different drives is invalid. So we move to
|
|
|
|
|
# self.tempdir to ensure that we stay on the same drive.
|
|
|
|
|
os.chdir(self.temp_dir)
|
|
|
|
|
nparser = parser.NginxParser(os.path.relpath(self.config_path))
|
|
|
|
|
self.assertEqual(nparser.root, self.config_path)
|
|
|
|
|
finally:
|
|
|
|
|
os.chdir(curr_dir)
|
2015-04-07 17:57:37 -04:00
|
|
|
|
|
|
|
|
def test_root_no_trailing_slash(self):
|
2017-05-02 20:56:56 -04:00
|
|
|
nparser = parser.NginxParser(self.config_path + os.path.sep)
|
2015-04-18 13:20:19 -04:00
|
|
|
self.assertEqual(nparser.root, self.config_path)
|
2015-04-07 17:57:37 -04:00
|
|
|
|
2015-04-15 17:44:51 -04:00
|
|
|
def test_load(self):
|
2015-04-07 17:57:37 -04:00
|
|
|
"""Test recursive conf file parsing.
|
|
|
|
|
|
|
|
|
|
"""
|
2017-05-02 20:56:56 -04:00
|
|
|
nparser = parser.NginxParser(self.config_path)
|
2015-04-18 13:20:19 -04:00
|
|
|
nparser.load()
|
Lint certbot code on Python 3, and update Pylint to the latest version (#7551)
Part of #7550
This PR makes appropriate corrections to run pylint on Python 3.
Why not keeping the dependencies unchanged and just run pylint on Python 3?
Because the old version of pylint breaks horribly on Python 3 because of unsupported version of astroid.
Why updating pylint + astroid to the latest version ?
Because this version only fixes some internal errors occuring during the lint of Certbot code, and is also ready to run gracefully on Python 3.8.
Why upgrading mypy ?
Because the old version does not support the new version of astroid required to run pylint correctly.
Why not upgrading mypy to its latest version ?
Because this latest version includes a new typshed version, that adds a lot of new type definitions, and brings dozens of new errors on the Certbot codebase. I would like to fix that in a future PR.
That said so, the work has been to find the correct set of new dependency versions, then configure pylint for sane configuration errors in our situation, disable irrelevant lintings errors, then fixing (or ignoring for good reason) the remaining mypy errors.
I also made PyLint and MyPy checks run correctly on Windows.
* Start configuration
* Reconfigure travis
* Suspend a check specific to python 3. Start fixing code.
* Repair call_args
* Fix return + elif lints
* Reconfigure development to run mainly on python3
* Remove incompatible Python 3.4 jobs
* Suspend pylint in some assertions
* Remove pylint in dev
* Take first mypy that supports typed-ast>=1.4.0 to limit the migration path
* Various return + else lint errors
* Find a set of deps that is working with current mypy version
* Update local oldest requirements
* Remove all current pylint errors
* Rebuild letsencrypt-auto
* Update mypy to fix pylint with new astroid version, and fix mypy issues
* Explain type: ignore
* Reconfigure tox, fix none path
* Simplify pinning
* Remove useless directive
* Remove debugging code
* Remove continue
* Update requirements
* Disable unsubscriptable-object check
* Disable one check, enabling two more
* Plug certbot dev version for oldest requirements
* Remove useless disable directives
* Remove useless no-member disable
* Remove no-else-* checks. Use elif in symetric branches.
* Add back assertion
* Add new line
* Remove unused pylint disable
* Remove other pylint disable
2019-12-10 17:12:50 -05:00
|
|
|
self.assertEqual({nparser.abs_path(x) for x in
|
2021-06-10 19:21:52 -04:00
|
|
|
['foo.conf', 'nginx.conf', 'server.conf', 'mime.types',
|
Lint certbot code on Python 3, and update Pylint to the latest version (#7551)
Part of #7550
This PR makes appropriate corrections to run pylint on Python 3.
Why not keeping the dependencies unchanged and just run pylint on Python 3?
Because the old version of pylint breaks horribly on Python 3 because of unsupported version of astroid.
Why updating pylint + astroid to the latest version ?
Because this version only fixes some internal errors occuring during the lint of Certbot code, and is also ready to run gracefully on Python 3.8.
Why upgrading mypy ?
Because the old version does not support the new version of astroid required to run pylint correctly.
Why not upgrading mypy to its latest version ?
Because this latest version includes a new typshed version, that adds a lot of new type definitions, and brings dozens of new errors on the Certbot codebase. I would like to fix that in a future PR.
That said so, the work has been to find the correct set of new dependency versions, then configure pylint for sane configuration errors in our situation, disable irrelevant lintings errors, then fixing (or ignoring for good reason) the remaining mypy errors.
I also made PyLint and MyPy checks run correctly on Windows.
* Start configuration
* Reconfigure travis
* Suspend a check specific to python 3. Start fixing code.
* Repair call_args
* Fix return + elif lints
* Reconfigure development to run mainly on python3
* Remove incompatible Python 3.4 jobs
* Suspend pylint in some assertions
* Remove pylint in dev
* Take first mypy that supports typed-ast>=1.4.0 to limit the migration path
* Various return + else lint errors
* Find a set of deps that is working with current mypy version
* Update local oldest requirements
* Remove all current pylint errors
* Rebuild letsencrypt-auto
* Update mypy to fix pylint with new astroid version, and fix mypy issues
* Explain type: ignore
* Reconfigure tox, fix none path
* Simplify pinning
* Remove useless directive
* Remove debugging code
* Remove continue
* Update requirements
* Disable unsubscriptable-object check
* Disable one check, enabling two more
* Plug certbot dev version for oldest requirements
* Remove useless disable directives
* Remove useless no-member disable
* Remove no-else-* checks. Use elif in symetric branches.
* Add back assertion
* Add new line
* Remove unused pylint disable
* Remove other pylint disable
2019-12-10 17:12:50 -05:00
|
|
|
'sites-enabled/default',
|
2021-02-26 16:43:22 -05:00
|
|
|
'sites-enabled/both.com',
|
Lint certbot code on Python 3, and update Pylint to the latest version (#7551)
Part of #7550
This PR makes appropriate corrections to run pylint on Python 3.
Why not keeping the dependencies unchanged and just run pylint on Python 3?
Because the old version of pylint breaks horribly on Python 3 because of unsupported version of astroid.
Why updating pylint + astroid to the latest version ?
Because this version only fixes some internal errors occuring during the lint of Certbot code, and is also ready to run gracefully on Python 3.8.
Why upgrading mypy ?
Because the old version does not support the new version of astroid required to run pylint correctly.
Why not upgrading mypy to its latest version ?
Because this latest version includes a new typshed version, that adds a lot of new type definitions, and brings dozens of new errors on the Certbot codebase. I would like to fix that in a future PR.
That said so, the work has been to find the correct set of new dependency versions, then configure pylint for sane configuration errors in our situation, disable irrelevant lintings errors, then fixing (or ignoring for good reason) the remaining mypy errors.
I also made PyLint and MyPy checks run correctly on Windows.
* Start configuration
* Reconfigure travis
* Suspend a check specific to python 3. Start fixing code.
* Repair call_args
* Fix return + elif lints
* Reconfigure development to run mainly on python3
* Remove incompatible Python 3.4 jobs
* Suspend pylint in some assertions
* Remove pylint in dev
* Take first mypy that supports typed-ast>=1.4.0 to limit the migration path
* Various return + else lint errors
* Find a set of deps that is working with current mypy version
* Update local oldest requirements
* Remove all current pylint errors
* Rebuild letsencrypt-auto
* Update mypy to fix pylint with new astroid version, and fix mypy issues
* Explain type: ignore
* Reconfigure tox, fix none path
* Simplify pinning
* Remove useless directive
* Remove debugging code
* Remove continue
* Update requirements
* Disable unsubscriptable-object check
* Disable one check, enabling two more
* Plug certbot dev version for oldest requirements
* Remove useless disable directives
* Remove useless no-member disable
* Remove no-else-* checks. Use elif in symetric branches.
* Add back assertion
* Add new line
* Remove unused pylint disable
* Remove other pylint disable
2019-12-10 17:12:50 -05:00
|
|
|
'sites-enabled/example.com',
|
|
|
|
|
'sites-enabled/headers.com',
|
|
|
|
|
'sites-enabled/migration.com',
|
|
|
|
|
'sites-enabled/sslon.com',
|
|
|
|
|
'sites-enabled/globalssl.com',
|
|
|
|
|
'sites-enabled/ipv6.com',
|
2020-02-06 15:24:25 -05:00
|
|
|
'sites-enabled/ipv6ssl.com',
|
|
|
|
|
'sites-enabled/example.net']},
|
2015-04-18 13:20:19 -04:00
|
|
|
set(nparser.parsed.keys()))
|
2017-03-24 22:45:53 -04:00
|
|
|
self.assertEqual([['server_name', 'somename', 'alias', 'another.alias']],
|
2015-04-18 13:20:19 -04:00
|
|
|
nparser.parsed[nparser.abs_path('server.conf')])
|
2015-04-14 19:24:10 -04:00
|
|
|
self.assertEqual([[['server'], [['listen', '69.50.225.155:9000'],
|
|
|
|
|
['listen', '127.0.0.1'],
|
2015-04-10 21:17:17 -04:00
|
|
|
['server_name', '.example.com'],
|
|
|
|
|
['server_name', 'example.*']]]],
|
2015-04-18 13:20:19 -04:00
|
|
|
nparser.parsed[nparser.abs_path(
|
2015-04-07 17:57:37 -04:00
|
|
|
'sites-enabled/example.com')])
|
|
|
|
|
|
2015-04-07 19:22:34 -04:00
|
|
|
def test_abs_path(self):
|
2017-05-02 20:56:56 -04:00
|
|
|
nparser = parser.NginxParser(self.config_path)
|
2019-02-20 19:20:16 -05:00
|
|
|
if os.name != 'nt':
|
|
|
|
|
self.assertEqual('/etc/nginx/*', nparser.abs_path('/etc/nginx/*'))
|
|
|
|
|
self.assertEqual(os.path.join(self.config_path, 'foo/bar'),
|
|
|
|
|
nparser.abs_path('foo/bar'))
|
|
|
|
|
else:
|
|
|
|
|
self.assertEqual('C:\\etc\\nginx\\*', nparser.abs_path('C:\\etc\\nginx\\*'))
|
|
|
|
|
self.assertEqual(os.path.join(self.config_path, 'foo\\bar'),
|
|
|
|
|
nparser.abs_path('foo\\bar'))
|
|
|
|
|
|
2015-04-07 19:22:34 -04:00
|
|
|
|
|
|
|
|
def test_filedump(self):
|
2017-05-02 20:56:56 -04:00
|
|
|
nparser = parser.NginxParser(self.config_path)
|
2016-06-24 20:14:14 -04:00
|
|
|
nparser.filedump('test', lazy=False)
|
2015-04-07 19:22:34 -04:00
|
|
|
# pylint: disable=protected-access
|
2015-04-18 13:20:19 -04:00
|
|
|
parsed = nparser._parse_files(nparser.abs_path(
|
2015-04-07 19:22:34 -04:00
|
|
|
'sites-enabled/example.com.test'))
|
2021-06-10 19:21:52 -04:00
|
|
|
self.assertEqual(4, len(glob.glob(nparser.abs_path('*.test'))))
|
2021-02-26 16:43:22 -05:00
|
|
|
self.assertEqual(10, len(
|
2015-04-18 13:20:19 -04:00
|
|
|
glob.glob(nparser.abs_path('sites-enabled/*.test'))))
|
2015-04-14 19:24:10 -04:00
|
|
|
self.assertEqual([[['server'], [['listen', '69.50.225.155:9000'],
|
|
|
|
|
['listen', '127.0.0.1'],
|
2015-04-10 21:17:17 -04:00
|
|
|
['server_name', '.example.com'],
|
|
|
|
|
['server_name', 'example.*']]]],
|
2015-04-07 19:22:34 -04:00
|
|
|
parsed[0])
|
|
|
|
|
|
2016-09-26 16:13:29 -04:00
|
|
|
def test__do_for_subarray(self):
|
|
|
|
|
# pylint: disable=protected-access
|
|
|
|
|
mylists = [([[2], [3], [2]], [[0], [2]]),
|
|
|
|
|
([[2], [3], [4]], [[0]]),
|
|
|
|
|
([[4], [3], [2]], [[2]]),
|
|
|
|
|
([], []),
|
|
|
|
|
(2, []),
|
|
|
|
|
([[[2], [3], [2]], [[2], [3], [2]]],
|
|
|
|
|
[[0, 0], [0, 2], [1, 0], [1, 2]]),
|
|
|
|
|
([[[0], [3], [2]], [[2], [3], [2]]], [[0, 2], [1, 0], [1, 2]]),
|
|
|
|
|
([[[0], [3], [4]], [[2], [3], [2]]], [[1, 0], [1, 2]]),
|
|
|
|
|
([[[0], [3], [4]], [[5], [3], [2]]], [[1, 2]]),
|
|
|
|
|
([[[0], [3], [4]], [[5], [3], [0]]], [])]
|
|
|
|
|
|
|
|
|
|
for mylist, result in mylists:
|
2021-03-10 14:51:27 -05:00
|
|
|
paths: List[List[int]] = []
|
2016-09-26 16:13:29 -04:00
|
|
|
parser._do_for_subarray(mylist,
|
|
|
|
|
lambda x: isinstance(x, list) and
|
|
|
|
|
len(x) >= 1 and
|
|
|
|
|
x[0] == 2,
|
|
|
|
|
lambda x, y, pts=paths: pts.append(y))
|
|
|
|
|
self.assertEqual(paths, result)
|
|
|
|
|
|
2016-12-05 22:17:04 -05:00
|
|
|
def test_get_vhosts_global_ssl(self):
|
2017-05-02 20:56:56 -04:00
|
|
|
nparser = parser.NginxParser(self.config_path)
|
2016-12-05 22:17:04 -05:00
|
|
|
vhosts = nparser.get_vhosts()
|
|
|
|
|
|
|
|
|
|
vhost = obj.VirtualHost(nparser.abs_path('sites-enabled/globalssl.com'),
|
2017-12-06 20:45:20 -05:00
|
|
|
[obj.Addr('4.8.2.6', '57', True, False,
|
|
|
|
|
False, False)],
|
2020-04-13 13:41:39 -04:00
|
|
|
True, True, {'globalssl.com'}, [], [0])
|
2016-12-05 22:17:04 -05:00
|
|
|
|
|
|
|
|
globalssl_com = [x for x in vhosts if 'globalssl.com' in x.filep][0]
|
|
|
|
|
self.assertEqual(vhost, globalssl_com)
|
2016-09-26 16:13:29 -04:00
|
|
|
|
2015-04-10 21:17:17 -04:00
|
|
|
def test_get_vhosts(self):
|
2017-05-02 20:56:56 -04:00
|
|
|
nparser = parser.NginxParser(self.config_path)
|
2015-04-18 13:20:19 -04:00
|
|
|
vhosts = nparser.get_vhosts()
|
|
|
|
|
|
|
|
|
|
vhost1 = obj.VirtualHost(nparser.abs_path('nginx.conf'),
|
2017-12-06 20:45:20 -05:00
|
|
|
[obj.Addr('', '8080', False, False,
|
|
|
|
|
False, False)],
|
2015-04-18 13:20:19 -04:00
|
|
|
False, True,
|
2020-04-13 13:41:39 -04:00
|
|
|
{'localhost',
|
|
|
|
|
r'~^(www\.)?(example|bar)\.'},
|
2017-04-26 21:44:06 -04:00
|
|
|
[], [10, 1, 9])
|
2015-04-18 13:20:19 -04:00
|
|
|
vhost2 = obj.VirtualHost(nparser.abs_path('nginx.conf'),
|
2017-12-06 20:45:20 -05:00
|
|
|
[obj.Addr('somename', '8080', False, False,
|
|
|
|
|
False, False),
|
|
|
|
|
obj.Addr('', '8000', False, False,
|
|
|
|
|
False, False)],
|
2015-04-18 13:20:19 -04:00
|
|
|
False, True,
|
2020-04-13 13:41:39 -04:00
|
|
|
{'somename', 'another.alias', 'alias'},
|
2017-04-26 21:44:06 -04:00
|
|
|
[], [10, 1, 12])
|
2015-04-18 13:20:19 -04:00
|
|
|
vhost3 = obj.VirtualHost(nparser.abs_path('sites-enabled/example.com'),
|
|
|
|
|
[obj.Addr('69.50.225.155', '9000',
|
2017-12-06 20:45:20 -05:00
|
|
|
False, False, False, False),
|
|
|
|
|
obj.Addr('127.0.0.1', '', False, False,
|
|
|
|
|
False, False)],
|
2015-04-18 13:20:19 -04:00
|
|
|
False, True,
|
2020-04-13 13:41:39 -04:00
|
|
|
{'.example.com', 'example.*'}, [], [0])
|
2015-04-18 13:20:19 -04:00
|
|
|
vhost4 = obj.VirtualHost(nparser.abs_path('sites-enabled/default'),
|
2017-12-06 20:45:20 -05:00
|
|
|
[obj.Addr('myhost', '', False, True,
|
|
|
|
|
False, False),
|
|
|
|
|
obj.Addr('otherhost', '', False, True,
|
|
|
|
|
False, False)],
|
2020-04-13 13:41:39 -04:00
|
|
|
False, True, {'www.example.org'},
|
2016-09-26 16:13:29 -04:00
|
|
|
[], [0])
|
2015-04-18 13:20:19 -04:00
|
|
|
vhost5 = obj.VirtualHost(nparser.abs_path('foo.conf'),
|
2017-12-06 20:45:20 -05:00
|
|
|
[obj.Addr('*', '80', True, True,
|
|
|
|
|
False, False)],
|
2020-04-13 13:41:39 -04:00
|
|
|
True, True, {'*.www.foo.com',
|
|
|
|
|
'*.www.example.com'},
|
2016-09-26 16:13:29 -04:00
|
|
|
[], [2, 1, 0])
|
2015-04-10 21:17:17 -04:00
|
|
|
|
2021-02-26 16:43:22 -05:00
|
|
|
self.assertEqual(19, len(vhosts))
|
2015-04-17 20:05:00 -04:00
|
|
|
example_com = [x for x in vhosts if 'example.com' in x.filep][0]
|
2015-04-10 21:17:17 -04:00
|
|
|
self.assertEqual(vhost3, example_com)
|
2015-04-17 20:05:00 -04:00
|
|
|
default = [x for x in vhosts if 'default' in x.filep][0]
|
2015-04-10 21:17:17 -04:00
|
|
|
self.assertEqual(vhost4, default)
|
2015-04-17 20:05:00 -04:00
|
|
|
fooconf = [x for x in vhosts if 'foo.conf' in x.filep][0]
|
|
|
|
|
self.assertEqual(vhost5, fooconf)
|
|
|
|
|
localhost = [x for x in vhosts if 'localhost' in x.names][0]
|
2016-07-19 13:12:47 -04:00
|
|
|
self.assertEqual(vhost1, localhost)
|
2015-04-17 20:05:00 -04:00
|
|
|
somename = [x for x in vhosts if 'somename' in x.names][0]
|
2016-07-19 13:12:47 -04:00
|
|
|
self.assertEqual(vhost2, somename)
|
2015-04-10 21:17:17 -04:00
|
|
|
|
2016-09-29 19:16:07 -04:00
|
|
|
def test_has_ssl_on_directive(self):
|
2017-05-02 20:56:56 -04:00
|
|
|
nparser = parser.NginxParser(self.config_path)
|
2016-09-29 19:16:07 -04:00
|
|
|
mock_vhost = obj.VirtualHost(None, None, None, None, None,
|
|
|
|
|
[['listen', 'myhost default_server'],
|
|
|
|
|
['server_name', 'www.example.org'],
|
|
|
|
|
[['location', '/'], [['root', 'html'], ['index', 'index.html index.htm']]]
|
|
|
|
|
], None)
|
|
|
|
|
self.assertFalse(nparser.has_ssl_on_directive(mock_vhost))
|
2017-03-24 22:45:53 -04:00
|
|
|
mock_vhost.raw = [['listen', '*:80', 'default_server', 'ssl'],
|
|
|
|
|
['server_name', '*.www.foo.com', '*.www.example.com'],
|
2016-09-29 19:16:07 -04:00
|
|
|
['root', '/home/ubuntu/sites/foo/']]
|
|
|
|
|
self.assertFalse(nparser.has_ssl_on_directive(mock_vhost))
|
|
|
|
|
mock_vhost.raw = [['listen', '80 ssl'],
|
2017-03-24 22:45:53 -04:00
|
|
|
['server_name', '*.www.foo.com', '*.www.example.com']]
|
2016-09-29 19:16:07 -04:00
|
|
|
self.assertFalse(nparser.has_ssl_on_directive(mock_vhost))
|
|
|
|
|
mock_vhost.raw = [['listen', '80'],
|
|
|
|
|
['ssl', 'on'],
|
2017-03-24 22:45:53 -04:00
|
|
|
['server_name', '*.www.foo.com', '*.www.example.com']]
|
2022-01-04 17:59:58 -05:00
|
|
|
self.assertIs(nparser.has_ssl_on_directive(mock_vhost), True)
|
2018-03-16 18:27:39 -04:00
|
|
|
|
|
|
|
|
def test_remove_server_directives(self):
|
|
|
|
|
nparser = parser.NginxParser(self.config_path)
|
|
|
|
|
mock_vhost = obj.VirtualHost(nparser.abs_path('nginx.conf'),
|
|
|
|
|
None, None, None,
|
2020-04-13 13:41:39 -04:00
|
|
|
{'localhost',
|
|
|
|
|
r'~^(www\.)?(example|bar)\.'},
|
2018-03-16 18:27:39 -04:00
|
|
|
None, [10, 1, 9])
|
|
|
|
|
example_com = nparser.abs_path('sites-enabled/example.com')
|
2020-04-13 13:41:39 -04:00
|
|
|
names = {'.example.com', 'example.*'}
|
2018-03-16 18:27:39 -04:00
|
|
|
mock_vhost.filep = example_com
|
|
|
|
|
mock_vhost.names = names
|
|
|
|
|
mock_vhost.path = [0]
|
|
|
|
|
nparser.add_server_directives(mock_vhost,
|
|
|
|
|
[['foo', 'bar'], ['ssl_certificate',
|
2018-03-23 19:30:13 -04:00
|
|
|
'/etc/ssl/cert2.pem']])
|
2018-03-16 18:27:39 -04:00
|
|
|
nparser.remove_server_directives(mock_vhost, 'foo')
|
|
|
|
|
nparser.remove_server_directives(mock_vhost, 'ssl_certificate')
|
|
|
|
|
self.assertEqual(nparser.parsed[example_com],
|
|
|
|
|
[[['server'], [['listen', '69.50.225.155:9000'],
|
|
|
|
|
['listen', '127.0.0.1'],
|
|
|
|
|
['server_name', '.example.com'],
|
|
|
|
|
['server_name', 'example.*'],
|
|
|
|
|
[]]]])
|
|
|
|
|
|
2015-04-10 21:17:17 -04:00
|
|
|
def test_add_server_directives(self):
|
2017-05-02 20:56:56 -04:00
|
|
|
nparser = parser.NginxParser(self.config_path)
|
2016-09-26 16:13:29 -04:00
|
|
|
mock_vhost = obj.VirtualHost(nparser.abs_path('nginx.conf'),
|
|
|
|
|
None, None, None,
|
2020-04-13 13:41:39 -04:00
|
|
|
{'localhost',
|
|
|
|
|
r'~^(www\.)?(example|bar)\.'},
|
2017-04-26 21:44:06 -04:00
|
|
|
None, [10, 1, 9])
|
2016-09-26 16:13:29 -04:00
|
|
|
nparser.add_server_directives(mock_vhost,
|
2016-06-17 17:39:55 -04:00
|
|
|
[['foo', 'bar'], ['\n ', 'ssl_certificate', ' ',
|
2018-03-23 19:30:13 -04:00
|
|
|
'/etc/ssl/cert.pem']])
|
2015-10-11 13:20:08 -04:00
|
|
|
ssl_re = re.compile(r'\n\s+ssl_certificate /etc/ssl/cert.pem')
|
|
|
|
|
dump = nginxparser.dumps(nparser.parsed[nparser.abs_path('nginx.conf')])
|
|
|
|
|
self.assertEqual(1, len(re.findall(ssl_re, dump)))
|
2015-10-11 15:19:39 -04:00
|
|
|
|
2016-09-26 16:13:29 -04:00
|
|
|
example_com = nparser.abs_path('sites-enabled/example.com')
|
2020-04-13 13:41:39 -04:00
|
|
|
names = {'.example.com', 'example.*'}
|
2016-09-26 16:13:29 -04:00
|
|
|
mock_vhost.filep = example_com
|
|
|
|
|
mock_vhost.names = names
|
|
|
|
|
mock_vhost.path = [0]
|
|
|
|
|
nparser.add_server_directives(mock_vhost,
|
2015-04-18 13:20:19 -04:00
|
|
|
[['foo', 'bar'], ['ssl_certificate',
|
2018-03-23 19:30:13 -04:00
|
|
|
'/etc/ssl/cert2.pem']])
|
|
|
|
|
nparser.add_server_directives(mock_vhost, [['foo', 'bar']])
|
2019-11-25 17:30:24 -05:00
|
|
|
from certbot_nginx._internal.parser import COMMENT
|
2016-09-26 16:13:29 -04:00
|
|
|
self.assertEqual(nparser.parsed[example_com],
|
|
|
|
|
[[['server'], [['listen', '69.50.225.155:9000'],
|
|
|
|
|
['listen', '127.0.0.1'],
|
|
|
|
|
['server_name', '.example.com'],
|
|
|
|
|
['server_name', 'example.*'],
|
|
|
|
|
['foo', 'bar'],
|
|
|
|
|
['#', COMMENT],
|
|
|
|
|
['ssl_certificate', '/etc/ssl/cert2.pem'],
|
|
|
|
|
['#', COMMENT], [], []
|
|
|
|
|
]]])
|
|
|
|
|
|
|
|
|
|
server_conf = nparser.abs_path('server.conf')
|
2020-04-13 13:41:39 -04:00
|
|
|
names = {'alias', 'another.alias', 'somename'}
|
2016-09-26 16:13:29 -04:00
|
|
|
mock_vhost.filep = server_conf
|
|
|
|
|
mock_vhost.names = names
|
|
|
|
|
mock_vhost.path = []
|
|
|
|
|
self.assertRaises(errors.MisconfigurationError,
|
|
|
|
|
nparser.add_server_directives,
|
|
|
|
|
mock_vhost,
|
|
|
|
|
[['foo', 'bar'],
|
2018-03-23 19:30:13 -04:00
|
|
|
['ssl_certificate', '/etc/ssl/cert2.pem']])
|
2015-10-11 15:19:39 -04:00
|
|
|
|
2017-05-15 18:30:50 -04:00
|
|
|
def test_comment_is_repeatable(self):
|
|
|
|
|
nparser = parser.NginxParser(self.config_path)
|
|
|
|
|
example_com = nparser.abs_path('sites-enabled/example.com')
|
|
|
|
|
mock_vhost = obj.VirtualHost(example_com,
|
|
|
|
|
None, None, None,
|
2020-04-13 13:41:39 -04:00
|
|
|
{'.example.com', 'example.*'},
|
2017-05-15 18:30:50 -04:00
|
|
|
None, [0])
|
|
|
|
|
nparser.add_server_directives(mock_vhost,
|
2018-03-23 19:30:13 -04:00
|
|
|
[['\n ', '#', ' ', 'what a nice comment']])
|
2017-05-15 18:30:50 -04:00
|
|
|
nparser.add_server_directives(mock_vhost,
|
|
|
|
|
[['\n ', 'include', ' ',
|
2018-03-23 19:30:13 -04:00
|
|
|
nparser.abs_path('comment_in_file.conf')]])
|
2019-11-25 17:30:24 -05:00
|
|
|
from certbot_nginx._internal.parser import COMMENT
|
2017-05-15 18:30:50 -04:00
|
|
|
self.assertEqual(nparser.parsed[example_com],
|
|
|
|
|
[[['server'], [['listen', '69.50.225.155:9000'],
|
|
|
|
|
['listen', '127.0.0.1'],
|
|
|
|
|
['server_name', '.example.com'],
|
|
|
|
|
['server_name', 'example.*'],
|
|
|
|
|
['#', ' ', 'what a nice comment'],
|
|
|
|
|
[],
|
|
|
|
|
['include', nparser.abs_path('comment_in_file.conf')],
|
|
|
|
|
['#', COMMENT],
|
|
|
|
|
[]]]]
|
|
|
|
|
)
|
|
|
|
|
|
2015-04-14 01:57:06 -04:00
|
|
|
def test_replace_server_directives(self):
|
2017-05-02 20:56:56 -04:00
|
|
|
nparser = parser.NginxParser(self.config_path)
|
2020-04-13 13:41:39 -04:00
|
|
|
target = {'.example.com', 'example.*'}
|
2015-04-18 13:20:19 -04:00
|
|
|
filep = nparser.abs_path('sites-enabled/example.com')
|
2016-09-26 16:13:29 -04:00
|
|
|
mock_vhost = obj.VirtualHost(filep, None, None, None, target, None, [0])
|
2018-03-23 19:30:13 -04:00
|
|
|
nparser.update_or_add_server_directives(
|
|
|
|
|
mock_vhost, [['server_name', 'foobar.com']])
|
2019-11-25 17:30:24 -05:00
|
|
|
from certbot_nginx._internal.parser import COMMENT
|
2015-04-14 01:57:06 -04:00
|
|
|
self.assertEqual(
|
2015-04-18 13:20:19 -04:00
|
|
|
nparser.parsed[filep],
|
2015-04-14 19:24:10 -04:00
|
|
|
[[['server'], [['listen', '69.50.225.155:9000'],
|
|
|
|
|
['listen', '127.0.0.1'],
|
2016-08-16 21:50:18 -04:00
|
|
|
['server_name', 'foobar.com'], ['#', COMMENT],
|
2016-08-05 18:37:01 -04:00
|
|
|
['server_name', 'example.*'], []
|
2016-01-01 19:35:57 -05:00
|
|
|
]]])
|
2020-04-13 13:41:39 -04:00
|
|
|
mock_vhost.names = {'foobar.com', 'example.*'}
|
2018-03-23 19:30:13 -04:00
|
|
|
nparser.update_or_add_server_directives(
|
|
|
|
|
mock_vhost, [['ssl_certificate', 'cert.pem']])
|
2017-07-26 16:57:25 -04:00
|
|
|
self.assertEqual(
|
|
|
|
|
nparser.parsed[filep],
|
|
|
|
|
[[['server'], [['listen', '69.50.225.155:9000'],
|
|
|
|
|
['listen', '127.0.0.1'],
|
|
|
|
|
['server_name', 'foobar.com'], ['#', COMMENT],
|
|
|
|
|
['server_name', 'example.*'], [],
|
|
|
|
|
['ssl_certificate', 'cert.pem'], ['#', COMMENT], [],
|
|
|
|
|
]]])
|
2015-04-10 21:17:17 -04:00
|
|
|
|
|
|
|
|
def test_get_best_match(self):
|
2015-04-14 01:57:06 -04:00
|
|
|
target_name = 'www.eff.org'
|
2020-04-13 13:41:39 -04:00
|
|
|
names = [{'www.eff.org', 'irrelevant.long.name.eff.org', '*.org'},
|
|
|
|
|
{'eff.org', 'ww2.eff.org', 'test.www.eff.org'},
|
|
|
|
|
{'*.eff.org', '.www.eff.org'},
|
|
|
|
|
{'.eff.org', '*.org'},
|
|
|
|
|
{'www.eff.', 'www.eff.*', '*.www.eff.org'},
|
|
|
|
|
{'example.com', r'~^(www\.)?(eff.+)', '*.eff.*'},
|
|
|
|
|
{'*', r'~^(www\.)?(eff.+)'},
|
|
|
|
|
{'www.*', r'~^(www\.)?(eff.+)', '.test.eff.org'},
|
|
|
|
|
{'*.org', r'*.eff.org', 'www.eff.*'},
|
|
|
|
|
{'*.www.eff.org', 'www.*'},
|
|
|
|
|
{'*.org'},
|
|
|
|
|
set(),
|
2020-09-08 17:14:54 -04:00
|
|
|
{'example.com'},
|
|
|
|
|
{'www.Eff.org'},
|
|
|
|
|
{'.efF.org'}]
|
2015-04-14 01:57:06 -04:00
|
|
|
winners = [('exact', 'www.eff.org'),
|
|
|
|
|
(None, None),
|
|
|
|
|
('exact', '.www.eff.org'),
|
|
|
|
|
('wildcard_start', '.eff.org'),
|
|
|
|
|
('wildcard_end', 'www.eff.*'),
|
2015-04-17 20:05:00 -04:00
|
|
|
('regex', r'~^(www\.)?(eff.+)'),
|
2015-04-14 01:57:06 -04:00
|
|
|
('wildcard_start', '*'),
|
|
|
|
|
('wildcard_end', 'www.*'),
|
|
|
|
|
('wildcard_start', '*.eff.org'),
|
|
|
|
|
('wildcard_end', 'www.*'),
|
|
|
|
|
('wildcard_start', '*.org'),
|
|
|
|
|
(None, None),
|
2020-09-08 17:14:54 -04:00
|
|
|
(None, None),
|
|
|
|
|
('exact', 'www.Eff.org'),
|
|
|
|
|
('wildcard_start', '.efF.org')]
|
2015-04-14 01:57:06 -04:00
|
|
|
|
|
|
|
|
for i, winner in enumerate(winners):
|
2015-04-18 13:20:19 -04:00
|
|
|
self.assertEqual(winner,
|
|
|
|
|
parser.get_best_match(target_name, names[i]))
|
2015-04-07 17:57:37 -04:00
|
|
|
|
2016-08-08 18:45:49 -04:00
|
|
|
def test_comment_directive(self):
|
|
|
|
|
# pylint: disable=protected-access
|
|
|
|
|
block = nginxparser.UnspacedList([
|
|
|
|
|
["\n", "a", " ", "b", "\n"],
|
|
|
|
|
["c", " ", "d"],
|
|
|
|
|
["\n", "e", " ", "f"]])
|
2019-11-25 17:30:24 -05:00
|
|
|
from certbot_nginx._internal.parser import comment_directive, COMMENT_BLOCK
|
2017-12-07 12:48:54 -05:00
|
|
|
comment_directive(block, 1)
|
|
|
|
|
comment_directive(block, 0)
|
2016-08-08 18:45:49 -04:00
|
|
|
self.assertEqual(block.spaced, [
|
|
|
|
|
["\n", "a", " ", "b", "\n"],
|
2016-08-16 20:45:43 -04:00
|
|
|
COMMENT_BLOCK,
|
2016-08-08 18:45:49 -04:00
|
|
|
"\n",
|
|
|
|
|
["c", " ", "d"],
|
2016-08-16 20:45:43 -04:00
|
|
|
COMMENT_BLOCK,
|
2016-08-08 18:45:49 -04:00
|
|
|
["\n", "e", " ", "f"]])
|
|
|
|
|
|
2017-05-02 21:37:54 -04:00
|
|
|
def test_comment_out_directive(self):
|
|
|
|
|
server_block = nginxparser.loads("""
|
|
|
|
|
server {
|
|
|
|
|
listen 80;
|
|
|
|
|
root /var/www/html;
|
|
|
|
|
index star.html;
|
|
|
|
|
|
|
|
|
|
server_name *.functorkitten.xyz;
|
|
|
|
|
ssl_session_timeout 1440m; ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
|
|
|
|
|
|
|
|
|
|
ssl_prefer_server_ciphers on;
|
|
|
|
|
}""")
|
|
|
|
|
block = server_block[0][1]
|
2019-11-25 17:30:24 -05:00
|
|
|
from certbot_nginx._internal.parser import _comment_out_directive
|
2017-05-02 21:37:54 -04:00
|
|
|
_comment_out_directive(block, 4, "blah1")
|
|
|
|
|
_comment_out_directive(block, 5, "blah2")
|
|
|
|
|
_comment_out_directive(block, 6, "blah3")
|
|
|
|
|
self.assertEqual(block.spaced, [
|
|
|
|
|
['\n ', 'listen', ' ', '80'],
|
|
|
|
|
['\n ', 'root', ' ', '/var/www/html'],
|
|
|
|
|
['\n ', 'index', ' ', 'star.html'],
|
|
|
|
|
['\n\n ', 'server_name', ' ', '*.functorkitten.xyz'],
|
|
|
|
|
['\n ', '#', ' ssl_session_timeout 1440m; # duplicated in blah1'],
|
|
|
|
|
[' ', '#', ' ssl_protocols TLSv1 TLSv1.1 TLSv1.2; # duplicated in blah2'],
|
|
|
|
|
['\n\n ', '#', ' ssl_prefer_server_ciphers on; # duplicated in blah3'],
|
|
|
|
|
'\n '])
|
|
|
|
|
|
2016-12-05 22:17:04 -05:00
|
|
|
def test_parse_server_raw_ssl(self):
|
|
|
|
|
server = parser._parse_server_raw([ #pylint: disable=protected-access
|
2016-02-20 04:38:45 -05:00
|
|
|
['listen', '443']
|
|
|
|
|
])
|
2016-01-07 11:27:40 -05:00
|
|
|
self.assertFalse(server['ssl'])
|
|
|
|
|
|
2016-12-05 22:17:04 -05:00
|
|
|
server = parser._parse_server_raw([ #pylint: disable=protected-access
|
2017-03-24 22:45:53 -04:00
|
|
|
['listen', '443', 'ssl']
|
2016-02-20 04:38:45 -05:00
|
|
|
])
|
2016-01-07 11:27:40 -05:00
|
|
|
self.assertTrue(server['ssl'])
|
|
|
|
|
|
2016-12-05 22:17:04 -05:00
|
|
|
server = parser._parse_server_raw([ #pylint: disable=protected-access
|
2016-02-20 04:38:45 -05:00
|
|
|
['listen', '443'], ['ssl', 'off']
|
|
|
|
|
])
|
2016-01-07 11:27:40 -05:00
|
|
|
self.assertFalse(server['ssl'])
|
|
|
|
|
|
2016-12-05 22:17:04 -05:00
|
|
|
server = parser._parse_server_raw([ #pylint: disable=protected-access
|
2016-02-20 04:38:45 -05:00
|
|
|
['listen', '443'], ['ssl', 'on']
|
|
|
|
|
])
|
2016-01-07 11:27:40 -05:00
|
|
|
self.assertTrue(server['ssl'])
|
2015-04-07 17:57:37 -04:00
|
|
|
|
2017-02-27 16:35:29 -05:00
|
|
|
def test_parse_server_raw_unix(self):
|
|
|
|
|
server = parser._parse_server_raw([ #pylint: disable=protected-access
|
|
|
|
|
['listen', 'unix:/var/run/nginx.sock']
|
|
|
|
|
])
|
|
|
|
|
self.assertEqual(len(server['addrs']), 0)
|
|
|
|
|
|
2016-12-05 22:17:04 -05:00
|
|
|
def test_parse_server_global_ssl_applied(self):
|
2017-05-02 20:56:56 -04:00
|
|
|
nparser = parser.NginxParser(self.config_path)
|
2016-12-05 22:17:04 -05:00
|
|
|
server = nparser.parse_server([
|
|
|
|
|
['listen', '443']
|
|
|
|
|
])
|
|
|
|
|
self.assertTrue(server['ssl'])
|
|
|
|
|
|
2017-12-07 12:48:54 -05:00
|
|
|
def test_duplicate_vhost(self):
|
2017-12-06 20:45:20 -05:00
|
|
|
nparser = parser.NginxParser(self.config_path)
|
|
|
|
|
|
|
|
|
|
vhosts = nparser.get_vhosts()
|
|
|
|
|
default = [x for x in vhosts if 'default' in x.filep][0]
|
2018-03-27 18:11:39 -04:00
|
|
|
new_vhost = nparser.duplicate_vhost(default, remove_singleton_listen_params=True)
|
2017-12-06 20:45:20 -05:00
|
|
|
nparser.filedump(ext='')
|
|
|
|
|
|
|
|
|
|
# check properties of new vhost
|
2022-01-04 17:59:58 -05:00
|
|
|
self.assertIs(next(iter(new_vhost.addrs)).default, False)
|
2017-12-06 20:45:20 -05:00
|
|
|
self.assertNotEqual(new_vhost.path, default.path)
|
|
|
|
|
|
|
|
|
|
# check that things are written to file correctly
|
|
|
|
|
new_nparser = parser.NginxParser(self.config_path)
|
|
|
|
|
new_vhosts = new_nparser.get_vhosts()
|
|
|
|
|
new_defaults = [x for x in new_vhosts if 'default' in x.filep]
|
|
|
|
|
self.assertEqual(len(new_defaults), 2)
|
|
|
|
|
new_vhost_parsed = new_defaults[1]
|
2022-01-04 17:59:58 -05:00
|
|
|
self.assertIs(next(iter(new_vhost_parsed.addrs)).default, False)
|
2017-12-06 20:45:20 -05:00
|
|
|
self.assertEqual(next(iter(default.names)), next(iter(new_vhost_parsed.names)))
|
|
|
|
|
self.assertEqual(len(default.raw), len(new_vhost_parsed.raw))
|
|
|
|
|
self.assertTrue(next(iter(default.addrs)).super_eq(next(iter(new_vhost_parsed.addrs))))
|
|
|
|
|
|
2018-03-27 18:11:39 -04:00
|
|
|
def test_duplicate_vhost_remove_ipv6only(self):
|
|
|
|
|
nparser = parser.NginxParser(self.config_path)
|
|
|
|
|
|
|
|
|
|
vhosts = nparser.get_vhosts()
|
|
|
|
|
ipv6ssl = [x for x in vhosts if 'ipv6ssl' in x.filep][0]
|
|
|
|
|
new_vhost = nparser.duplicate_vhost(ipv6ssl, remove_singleton_listen_params=True)
|
|
|
|
|
nparser.filedump(ext='')
|
|
|
|
|
|
|
|
|
|
for addr in new_vhost.addrs:
|
|
|
|
|
self.assertFalse(addr.ipv6only)
|
|
|
|
|
|
|
|
|
|
identical_vhost = nparser.duplicate_vhost(ipv6ssl, remove_singleton_listen_params=False)
|
|
|
|
|
nparser.filedump(ext='')
|
|
|
|
|
|
|
|
|
|
called = False
|
|
|
|
|
for addr in identical_vhost.addrs:
|
|
|
|
|
if addr.ipv6:
|
|
|
|
|
self.assertTrue(addr.ipv6only)
|
|
|
|
|
called = True
|
|
|
|
|
self.assertTrue(called)
|
|
|
|
|
|
2018-10-31 03:48:01 -04:00
|
|
|
def test_valid_unicode_characters(self):
|
|
|
|
|
nparser = parser.NginxParser(self.config_path)
|
2020-02-23 12:23:46 -05:00
|
|
|
path = nparser.abs_path('valid_unicode_comments.conf')
|
2020-02-23 12:48:21 -05:00
|
|
|
parsed = nparser._parse_files(path) # pylint: disable=protected-access
|
2018-10-31 03:48:01 -04:00
|
|
|
self.assertEqual(['server'], parsed[0][2][0])
|
|
|
|
|
self.assertEqual(['listen', '80'], parsed[0][2][1][3])
|
|
|
|
|
|
2020-11-27 12:15:27 -05:00
|
|
|
def test_valid_unicode_roundtrip(self):
|
|
|
|
|
"""This tests the parser's ability to load and save a config containing Unicode"""
|
|
|
|
|
nparser = parser.NginxParser(self.config_path)
|
|
|
|
|
nparser._parse_files(
|
|
|
|
|
nparser.abs_path('valid_unicode_comments.conf')
|
|
|
|
|
) # pylint: disable=protected-access
|
|
|
|
|
nparser.filedump(lazy=False)
|
|
|
|
|
|
2018-10-31 03:48:01 -04:00
|
|
|
def test_invalid_unicode_characters(self):
|
2020-02-23 12:47:44 -05:00
|
|
|
with self.assertLogs() as log:
|
|
|
|
|
nparser = parser.NginxParser(self.config_path)
|
|
|
|
|
path = nparser.abs_path('invalid_unicode_comments.conf')
|
|
|
|
|
parsed = nparser._parse_files(path) # pylint: disable=protected-access
|
|
|
|
|
|
2018-10-31 03:48:01 -04:00
|
|
|
self.assertEqual([], parsed)
|
2020-02-24 23:26:36 -05:00
|
|
|
self.assertTrue(any(
|
|
|
|
|
('invalid character' in output) and ('UTF-8' in output)
|
2020-02-23 12:47:44 -05:00
|
|
|
for output in log.output
|
2020-02-24 23:26:36 -05:00
|
|
|
))
|
2018-03-27 18:11:39 -04:00
|
|
|
|
2020-02-23 12:46:27 -05:00
|
|
|
def test_valid_unicode_characters_in_ssl_options(self):
|
|
|
|
|
nparser = parser.NginxParser(self.config_path)
|
|
|
|
|
path = nparser.abs_path('valid_unicode_comments.conf')
|
|
|
|
|
parsed = parser._parse_ssl_options(path) # pylint: disable=protected-access
|
|
|
|
|
self.assertEqual(['server'], parsed[2][0])
|
|
|
|
|
self.assertEqual(['listen', '80'], parsed[2][1][3])
|
|
|
|
|
|
|
|
|
|
def test_invalid_unicode_characters_in_ssl_options(self):
|
|
|
|
|
with self.assertLogs() as log:
|
|
|
|
|
nparser = parser.NginxParser(self.config_path)
|
|
|
|
|
path = nparser.abs_path('invalid_unicode_comments.conf')
|
|
|
|
|
parsed = parser._parse_ssl_options(path) # pylint: disable=protected-access
|
|
|
|
|
|
|
|
|
|
self.assertEqual([], parsed)
|
2020-02-24 23:26:36 -05:00
|
|
|
self.assertTrue(any(
|
|
|
|
|
('invalid character' in output) and ('UTF-8' in output)
|
2020-02-23 12:46:27 -05:00
|
|
|
for output in log.output
|
2020-02-24 23:26:36 -05:00
|
|
|
))
|
2016-01-25 11:39:46 -05:00
|
|
|
|
2022-01-04 17:59:58 -05:00
|
|
|
|
2015-04-07 17:57:37 -04:00
|
|
|
if __name__ == "__main__":
|
2015-05-10 13:34:25 -04:00
|
|
|
unittest.main() # pragma: no cover
|