commit 0efd180327c96a75d25744374e72bf11b5a5b586 Author: Damian Johnson atagar@torproject.org Date: Sat Apr 5 21:07:51 2014 -0700
Unit tests for the connect() function
One really neat thing about stealing arm's code is that I'd actually written a lot of good unit tests, so we can now move those to stem! Our connect* methods previously only had integ coverage, so this is a really nice improvement.
This also uncovered a few bugs from porting the function. --- stem/connection.py | 52 +++++++++++++-- test/settings.cfg | 1 + test/unit/connection/connect.py | 141 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 188 insertions(+), 6 deletions(-)
diff --git a/stem/connection.py b/stem/connection.py index 1304357..35524d7 100644 --- a/stem/connection.py +++ b/stem/connection.py @@ -147,6 +147,40 @@ AuthMethod = stem.util.enum.Enum("NONE", "PASSWORD", "COOKIE", "SAFECOOKIE", "UN CLIENT_HASH_CONSTANT = b"Tor safe cookie authentication controller-to-server hash" SERVER_HASH_CONSTANT = b"Tor safe cookie authentication server-to-controller hash"
+MISSING_PASSWORD_BUG_MSG = """ +BUG: You provided a password but despite this stem reported that it was +missing. This shouldn't happen - please let us know about it! + + http://bugs.torproject.org +""" + +UNRECOGNIZED_AUTH_TYPE_MSG = """ +Tor is using a type of authentication we do not recognize... + + {auth_methods} + +Please check that arm is up to date and if there is an existing issue on +'http://bugs.torproject.org'. If there isn't one then let us know! +""" + + +UNREADABLE_COOKIE_FILE_MSG = """ +We were unable to read tor's authentication cookie... + + Path: {path} + Issue: {issue} +""" + +WRONG_PORT_TYPE_MSG = """ +Please check in your torrc that {port} is the ControlPort. Maybe you +configured it to be the ORPort or SocksPort instead? +""" + +WRONG_SOCKET_TYPE_MSG = """ +Unable to connect to tor. Are you sure the interface you specified belongs to +tor? +""" + CONNECT_MESSAGES = { 'general_auth_failure': "Unable to authenticate: {error}", 'incorrect_password': "Incorrect password", @@ -156,6 +190,11 @@ CONNECT_MESSAGES = { 'tor_isnt_running': "Unable to connect to tor. Are you sure it's running?", 'unable_to_use_port': "Unable to connect to {address}:{port}: {error}", 'unable_to_use_socket': "Unable to connect to '{path}': {error}", + 'missing_password_bug': MISSING_PASSWORD_BUG_MSG.strip(), + 'uncrcognized_auth_type': UNRECOGNIZED_AUTH_TYPE_MSG.strip(), + 'unreadable_cookie_file': UNREADABLE_COOKIE_FILE_MSG.strip(), + 'wrong_port_type': WRONG_PORT_TYPE_MSG.strip(), + 'wrong_socket_type': WRONG_SOCKET_TYPE_MSG.strip(), }
@@ -232,7 +271,7 @@ def connect(control_port = ('127.0.0.1', 9051), control_socket = '/var/run/tor/c print error_msg return None
- return _connect(control_connection, password, chroot_path, controller) + return _connect_auth(control_connection, password, chroot_path, controller)
def connect_port(address = "127.0.0.1", port = 9051, password = None, chroot_path = None, controller = stem.control.Controller): @@ -261,7 +300,7 @@ def connect_port(address = "127.0.0.1", port = 9051, password = None, chroot_pat print exc return None
- return _connect(control_port, password, chroot_path, controller) + return _connect_auth(control_port, password, chroot_path, controller)
def connect_socket_file(path = "/var/run/tor/control", password = None, chroot_path = None, controller = stem.control.Controller): @@ -291,12 +330,13 @@ def connect_socket_file(path = "/var/run/tor/control", password = None, chroot_p print exc return None
- return _connect(control_socket, password, chroot_path, controller) + return _connect_auth(control_socket, password, chroot_path, controller)
-def _connect(control_socket, password, chroot_path, controller): +def _connect_auth(control_socket, password, chroot_path, controller): """ - Common implementation for the connect_* functions. + Helper for the connect_* functions that authenticates the socket and + constructs the controller.
:param stem.socket.ControlSocket control_socket: socket being authenticated to :param str password: passphrase to authenticate to the socket @@ -341,7 +381,7 @@ def _connect(control_socket, password, chroot_path, controller): control_socket.close() return None
- return _connect(control_socket, password, chroot_path, controller) + return _connect_auth(control_socket, password, chroot_path, controller) except UnreadableCookieFile as exc: print CONNECT_MESSAGES['unreadable_cookie_file'].format(path = exc.cookie_path, issue = str(exc)) control_socket.close() diff --git a/test/settings.cfg b/test/settings.cfg index f12311d..f0eb758 100644 --- a/test/settings.cfg +++ b/test/settings.cfg @@ -181,6 +181,7 @@ test.unit_tests |test.unit.response.protocolinfo.TestProtocolInfoResponse |test.unit.response.mapaddress.TestMapAddressResponse |test.unit.connection.authentication.TestAuthenticate +|test.unit.connection.connect.TestConnect |test.unit.control.controller.TestControl |test.unit.doctest.TestDocumentation
diff --git a/test/unit/connection/connect.py b/test/unit/connection/connect.py new file mode 100644 index 0000000..52039f2 --- /dev/null +++ b/test/unit/connection/connect.py @@ -0,0 +1,141 @@ +""" +Unit tests for the stem.connection.connect function. +""" + +import StringIO +import unittest + +from mock import Mock, patch + +import stem +import stem.connection +import stem.socket + + +class TestConnect(unittest.TestCase): + @patch('sys.stdout', new_callable = StringIO.StringIO) + @patch('stem.util.system.is_running') + @patch('os.path.exists', Mock(return_value = True)) + @patch('stem.socket.ControlSocketFile', Mock(side_effect = stem.SocketError('failed'))) + @patch('stem.socket.ControlPort', Mock(side_effect = stem.SocketError('failed'))) + @patch('stem.connection._connect_auth', Mock()) + def test_failue_with_the_default_endpoint(self, is_running_mock, stdout_mock): + is_running_mock.return_value = False + self._assert_connect_fails_with({}, stdout_mock, "Unable to connect to tor. Are you sure it's running?") + + is_running_mock.return_value = True + self._assert_connect_fails_with({}, stdout_mock, "Unable to connect to tor. Maybe it's running without a ControlPort?") + + @patch('sys.stdout', new_callable = StringIO.StringIO) + @patch('os.path.exists') + @patch('stem.util.system.is_running', Mock(return_value = True)) + @patch('stem.socket.ControlSocketFile', Mock(side_effect = stem.SocketError('failed'))) + @patch('stem.socket.ControlPort', Mock(side_effect = stem.SocketError('failed'))) + @patch('stem.connection._connect_auth', Mock()) + def test_failure_with_a_custom_endpoint(self, path_exists_mock, stdout_mock): + path_exists_mock.return_value = True + self._assert_connect_fails_with({'control_port': ('127.0.0.1', 80), 'control_socket': None}, stdout_mock, "Unable to connect to 127.0.0.1:80: failed") + self._assert_connect_fails_with({'control_port': None, 'control_socket': '/tmp/my_socket'}, stdout_mock, "Unable to connect to '/tmp/my_socket': failed") + + path_exists_mock.return_value = False + self._assert_connect_fails_with({'control_port': ('127.0.0.1', 80), 'control_socket': None}, stdout_mock, "Unable to connect to 127.0.0.1:80: failed") + self._assert_connect_fails_with({'control_port': None, 'control_socket': '/tmp/my_socket'}, stdout_mock, "The socket file you specified (/tmp/my_socket) doesn't exist") + + @patch('stem.socket.ControlPort') + @patch('os.path.exists', Mock(return_value = False)) + @patch('stem.connection._connect_auth', Mock()) + def test_getting_a_control_port(self, port_mock): + stem.connection.connect() + port_mock.assert_called_once_with('127.0.0.1', 9051) + port_mock.reset_mock() + + stem.connection.connect(control_port = ('255.0.0.10', 80), control_socket = None) + port_mock.assert_called_once_with('255.0.0.10', 80) + + @patch('stem.socket.ControlSocketFile') + @patch('os.path.exists', Mock(return_value = True)) + @patch('stem.connection._connect_auth', Mock()) + def test_getting_a_control_socket(self, socket_mock): + stem.connection.connect() + socket_mock.assert_called_once_with('/var/run/tor/control') + socket_mock.reset_mock() + + stem.connection.connect(control_port = None, control_socket = '/tmp/my_socket') + socket_mock.assert_called_once_with('/tmp/my_socket') + + def _assert_connect_fails_with(self, args, stdout_mock, msg): + result = stem.connection.connect(**args) + + if result is not None: + self.fail() + + stdout_output = stdout_mock.getvalue() + stdout_mock.truncate(0) + self.assertEqual(msg, stdout_output.strip()) + + @patch('stem.connection.authenticate') + def test_auth_success(self, authenticate_mock): + control_socket = Mock() + + stem.connection._connect_auth(control_socket, None, None, None) + authenticate_mock.assert_called_with(control_socket, None, None) + authenticate_mock.reset_mock() + + stem.connection._connect_auth(control_socket, 's3krit!!!', '/my/chroot', None) + authenticate_mock.assert_called_with(control_socket, 's3krit!!!', '/my/chroot') + + @patch('getpass.getpass') + @patch('stem.connection.authenticate') + def test_auth_success_with_password_prompt(self, authenticate_mock, getpass_mock): + control_socket = Mock() + + def authenticate_mock_func(controller, password, *args): + if password is None: + raise stem.connection.MissingPassword('no password') + elif password == 'my_password': + return None # success + else: + raise ValueError("Unexpected authenticate_mock input: %s" % password) + + authenticate_mock.side_effect = authenticate_mock_func + getpass_mock.return_value = 'my_password' + + stem.connection._connect_auth(control_socket, None, None, None) + authenticate_mock.assert_any_call(control_socket, None, None) + authenticate_mock.assert_any_call(control_socket, 'my_password', None) + + @patch('sys.stdout', new_callable = StringIO.StringIO) + @patch('stem.connection.authenticate') + def test_auth_failure(self, authenticate_mock, stdout_mock): + control_socket = stem.socket.ControlPort(connect = False) + + authenticate_mock.side_effect = stem.connection.IncorrectSocketType('unable to connect to socket') + self._assert_authenticate_fails_with(control_socket, stdout_mock, 'Please check in your torrc that 9051 is the ControlPort.') + + control_socket = stem.socket.ControlSocketFile(connect = False) + + self._assert_authenticate_fails_with(control_socket, stdout_mock, 'Are you sure the interface you specified belongs to') + + authenticate_mock.side_effect = stem.connection.UnrecognizedAuthMethods('unable to connect', ['telepathy']) + self._assert_authenticate_fails_with(control_socket, stdout_mock, 'Tor is using a type of authentication we do not recognize...\n\n telepathy') + + authenticate_mock.side_effect = stem.connection.IncorrectPassword('password rejected') + self._assert_authenticate_fails_with(control_socket, stdout_mock, 'Incorrect password') + + authenticate_mock.side_effect = stem.connection.UnreadableCookieFile('permission denied', '/tmp/my_cookie', False) + self._assert_authenticate_fails_with(control_socket, stdout_mock, "We were unable to read tor's authentication cookie...\n\n Path: /tmp/my_cookie\n Issue: permission denied") + + authenticate_mock.side_effect = stem.connection.OpenAuthRejected('crazy failure') + self._assert_authenticate_fails_with(control_socket, stdout_mock, 'Unable to authenticate: crazy failure') + + def _assert_authenticate_fails_with(self, control_socket, stdout_mock, msg): + result = stem.connection._connect_auth(control_socket, None, None, None) + + if result is not None: + self.fail() # _connect_auth() was successful + + stdout_output = stdout_mock.getvalue() + stdout_mock.truncate(0) + + if not msg in stdout_output: + self.fail("Expected...\n\n%s\n\n... which couldn't be found in...\n\n%s" % (msg, stdout_output))