[Author Prev][Author Next][Thread Prev][Thread Next][Author Index][Thread Index]
[tor-commits] [stem/master] Moving tor response classes into their own module
commit 1d7ab654ec473a5e54ea3253f82abdcadc93158f
Author: Damian Johnson <atagar@xxxxxxxxxxxxxx>
Date: Mon May 28 15:12:16 2012 -0700
Moving tor response classes into their own module
As we add more response classes it'll be messy to sprinkle them all about the
codebase. Making a single 'stem.response' module that'll contain them all.
These could probably do with some more love so I'll next see if I can make them
any tidier.
---
run_tests.py | 12 +-
stem/__init__.py | 2 +-
stem/connection.py | 164 +-----------------------------
stem/control.py | 67 +------------
stem/response/__init__.py | 46 +++++++++
stem/response/getinfo.py | 43 ++++++++
stem/response/protocolinfo.py | 131 ++++++++++++++++++++++++
test/integ/connection/__init__.py | 2 +-
test/integ/connection/protocolinfo.py | 138 -------------------------
test/integ/response/__init__.py | 6 +
test/integ/response/protocolinfo.py | 138 +++++++++++++++++++++++++
test/mocking.py | 8 +-
test/unit/connection/__init__.py | 2 +-
test/unit/connection/protocolinfo.py | 178 --------------------------------
test/unit/control/getinfo.py | 119 ----------------------
test/unit/response/__init__.py | 6 +
test/unit/response/getinfo.py | 120 ++++++++++++++++++++++
test/unit/response/protocolinfo.py | 180 +++++++++++++++++++++++++++++++++
18 files changed, 689 insertions(+), 673 deletions(-)
diff --git a/run_tests.py b/run_tests.py
index 13fb027..dea86c3 100755
--- a/run_tests.py
+++ b/run_tests.py
@@ -16,13 +16,13 @@ import test.output
import test.runner
import test.check_whitespace
import test.unit.connection.authentication
-import test.unit.connection.protocolinfo
-import test.unit.control.getinfo
import test.unit.socket.control_line
import test.unit.socket.control_message
import test.unit.descriptor.reader
import test.unit.descriptor.server_descriptor
import test.unit.descriptor.extrainfo_descriptor
+import test.unit.response.getinfo
+import test.unit.response.protocolinfo
import test.unit.util.conf
import test.unit.util.connection
import test.unit.util.enum
@@ -31,7 +31,6 @@ import test.unit.util.tor_tools
import test.unit.version
import test.integ.connection.authentication
import test.integ.connection.connect
-import test.integ.connection.protocolinfo
import test.integ.control.base_controller
import test.integ.control.controller
import test.integ.socket.control_message
@@ -39,6 +38,7 @@ import test.integ.socket.control_socket
import test.integ.descriptor.reader
import test.integ.descriptor.server_descriptor
import test.integ.descriptor.extrainfo_descriptor
+import test.integ.response.protocolinfo
import test.integ.util.conf
import test.integ.util.system
import test.integ.process
@@ -100,11 +100,11 @@ UNIT_TESTS = (
test.unit.descriptor.server_descriptor.TestServerDescriptor,
test.unit.descriptor.extrainfo_descriptor.TestExtraInfoDescriptor,
test.unit.version.TestVersion,
+ test.unit.response.getinfo.TestGetInfoResponse,
+ test.unit.response.protocolinfo.TestProtocolInfoResponse,
test.unit.socket.control_message.TestControlMessage,
test.unit.socket.control_line.TestControlLine,
test.unit.connection.authentication.TestAuthenticate,
- test.unit.connection.protocolinfo.TestProtocolInfoResponse,
- test.unit.control.getinfo.TestGetInfoResponse,
)
INTEG_TESTS = (
@@ -114,10 +114,10 @@ INTEG_TESTS = (
test.integ.descriptor.server_descriptor.TestServerDescriptor,
test.integ.descriptor.extrainfo_descriptor.TestExtraInfoDescriptor,
test.integ.version.TestVersion,
+ test.integ.response.protocolinfo.TestProtocolInfo,
test.integ.process.TestProcess,
test.integ.socket.control_socket.TestControlSocket,
test.integ.socket.control_message.TestControlMessage,
- test.integ.connection.protocolinfo.TestProtocolInfo,
test.integ.connection.authentication.TestAuthenticate,
test.integ.connection.connect.TestConnect,
test.integ.control.base_controller.TestBaseController,
diff --git a/stem/__init__.py b/stem/__init__.py
index ceb137f..30e4f81 100644
--- a/stem/__init__.py
+++ b/stem/__init__.py
@@ -2,5 +2,5 @@
Library for working with the tor process.
"""
-__all__ = ["descriptor", "util", "connection", "control", "process", "socket", "version"]
+__all__ = ["descriptor", "response", "util", "connection", "control", "process", "socket", "version"]
diff --git a/stem/connection.py b/stem/connection.py
index 983eb84..34c9c68 100644
--- a/stem/connection.py
+++ b/stem/connection.py
@@ -48,14 +48,6 @@ authenticate_password - Authenticates to a socket supporting password auth.
authenticate_cookie - Authenticates to a socket supporting cookie auth.
get_protocolinfo - Issues a PROTOCOLINFO query.
-ProtocolInfoResponse - Reply from a PROTOCOLINFO query.
- |- Attributes:
- | |- protocol_version
- | |- tor_version
- | |- auth_methods
- | |- unknown_auth_methods
- | +- cookie_path
- +- convert - parses a ControlMessage, turning it into a ProtocolInfoResponse
AuthenticationFailure - Base exception raised for authentication failures.
|- UnrecognizedAuthMethods - Authentication methods are unsupported.
@@ -84,6 +76,7 @@ import os
import getpass
import binascii
+import stem.response
import stem.socket
import stem.version
import stem.util.enum
@@ -295,7 +288,7 @@ def authenticate(controller, password = None, chroot_path = None, protocolinfo_r
password (str) - passphrase to present to the socket if it uses password
authentication (skips password auth if None)
chroot_path (str) - path prefix if in a chroot environment
- protocolinfo_response (stem.connection.ProtocolInfoResponse) -
+ protocolinfo_response (stem.response.protocolinfo.ProtocolInfoResponse) -
tor protocolinfo response, this is retrieved on our own if None
Raises:
@@ -640,7 +633,7 @@ def get_protocolinfo(controller):
tor controller connection
Returns:
- stem.connection.ProtocolInfoResponse provided by tor
+ stem.response.protocolinfo.ProtocolInfoResponse provided by tor
Raises:
stem.socket.ProtocolError if the PROTOCOLINFO response is malformed
@@ -664,7 +657,7 @@ def get_protocolinfo(controller):
except stem.socket.SocketClosed, exc:
raise stem.socket.SocketError(exc)
- ProtocolInfoResponse.convert(protocolinfo_response)
+ stem.response.convert("PROTOCOLINFO", protocolinfo_response)
# attempt to expand relative cookie paths via the control port or socket file
@@ -723,152 +716,3 @@ def _expand_cookie_path(protocolinfo_response, pid_resolver, pid_resolution_arg)
protocolinfo_response.cookie_path = cookie_path
-class ProtocolInfoResponse(stem.socket.ControlMessage):
- """
- Version one PROTOCOLINFO query response.
-
- According to the control spec the cookie_file is an absolute path. However,
- this often is not the case (especially for the Tor Browser Bundle)...
- https://trac.torproject.org/projects/tor/ticket/1101
-
- If the path is relative then we'll make an attempt (which may not work) to
- correct this.
-
- The protocol_version is the only mandatory data for a valid PROTOCOLINFO
- response, so all other values are None if undefined or empty if a collection.
-
- Attributes:
- protocol_version (int) - protocol version of the response
- tor_version (stem.version.Version) - version of the tor process
- auth_methods (tuple) - AuthMethod types that tor will accept
- unknown_auth_methods (tuple) - strings of unrecognized auth methods
- cookie_path (str) - path of tor's authentication cookie
- """
-
- def convert(control_message):
- """
- Parses a ControlMessage, performing an in-place conversion of it into a
- ProtocolInfoResponse.
-
- Arguments:
- control_message (stem.socket.ControlMessage) -
- message to be parsed as a PROTOCOLINFO reply
-
- Raises:
- stem.socket.ProtocolError the message isn't a proper PROTOCOLINFO response
- TypeError if argument isn't a ControlMessage
- """
-
- if isinstance(control_message, stem.socket.ControlMessage):
- control_message.__class__ = ProtocolInfoResponse
- control_message._parse_message()
- return control_message
- else:
- raise TypeError("Only able to convert stem.socket.ControlMessage instances")
-
- convert = staticmethod(convert)
-
- def _parse_message(self):
- # Example:
- # 250-PROTOCOLINFO 1
- # 250-AUTH METHODS=COOKIE COOKIEFILE="/home/atagar/.tor/control_auth_cookie"
- # 250-VERSION Tor="0.2.1.30"
- # 250 OK
-
- self.protocol_version = None
- self.tor_version = None
- self.cookie_path = None
-
- auth_methods, unknown_auth_methods = [], []
-
- # sanity check that we're a PROTOCOLINFO response
- if not list(self)[0].startswith("PROTOCOLINFO"):
- msg = "Message is not a PROTOCOLINFO response (%s)" % self
- raise stem.socket.ProtocolError(msg)
-
- for line in self:
- if line == "OK": break
- elif line.is_empty(): continue # blank line
-
- line_type = line.pop()
-
- if line_type == "PROTOCOLINFO":
- # Line format:
- # FirstLine = "PROTOCOLINFO" SP PIVERSION CRLF
- # PIVERSION = 1*DIGIT
-
- if line.is_empty():
- msg = "PROTOCOLINFO response's initial line is missing the protocol version: %s" % line
- raise stem.socket.ProtocolError(msg)
-
- piversion = line.pop()
-
- if not piversion.isdigit():
- msg = "PROTOCOLINFO response version is non-numeric: %s" % line
- raise stem.socket.ProtocolError(msg)
-
- self.protocol_version = int(piversion)
-
- # The piversion really should be "1" but, according to the spec, tor
- # does not necessarily need to provide the PROTOCOLINFO version that we
- # requested. Log if it's something we aren't expecting but still make
- # an effort to parse like a v1 response.
-
- if self.protocol_version != 1:
- log.info("We made a PROTOCOLINFO version 1 query but got a version %i response instead. We'll still try to use it, but this may cause problems." % self.protocol_version)
- elif line_type == "AUTH":
- # Line format:
- # AuthLine = "250-AUTH" SP "METHODS=" AuthMethod *("," AuthMethod)
- # *(SP "COOKIEFILE=" AuthCookieFile) CRLF
- # AuthMethod = "NULL" / "HASHEDPASSWORD" / "COOKIE"
- # AuthCookieFile = QuotedString
-
- # parse AuthMethod mapping
- if not line.is_next_mapping("METHODS"):
- msg = "PROTOCOLINFO response's AUTH line is missing its mandatory 'METHODS' mapping: %s" % line
- raise stem.socket.ProtocolError(msg)
-
- for method in line.pop_mapping()[1].split(","):
- if method == "NULL":
- auth_methods.append(AuthMethod.NONE)
- elif method == "HASHEDPASSWORD":
- auth_methods.append(AuthMethod.PASSWORD)
- elif method == "COOKIE":
- auth_methods.append(AuthMethod.COOKIE)
- else:
- unknown_auth_methods.append(method)
- message_id = "stem.connection.unknown_auth_%s" % method
- log.log_once(message_id, log.INFO, "PROTOCOLINFO response included a type of authentication that we don't recognize: %s" % method)
-
- # our auth_methods should have a single AuthMethod.UNKNOWN entry if
- # any unknown authentication methods exist
- if not AuthMethod.UNKNOWN in auth_methods:
- auth_methods.append(AuthMethod.UNKNOWN)
-
- # parse optional COOKIEFILE mapping (quoted and can have escapes)
- if line.is_next_mapping("COOKIEFILE", True, True):
- self.cookie_path = line.pop_mapping(True, True)[1]
-
- # attempt to expand relative cookie paths
- _expand_cookie_path(self, stem.util.system.get_pid_by_name, "tor")
- elif line_type == "VERSION":
- # Line format:
- # VersionLine = "250-VERSION" SP "Tor=" TorVersion OptArguments CRLF
- # TorVersion = QuotedString
-
- if not line.is_next_mapping("Tor", True):
- msg = "PROTOCOLINFO response's VERSION line is missing its mandatory tor version mapping: %s" % line
- raise stem.socket.ProtocolError(msg)
-
- torversion = line.pop_mapping(True)[1]
-
- try:
- self.tor_version = stem.version.Version(torversion)
- except ValueError, exc:
- raise stem.socket.ProtocolError(exc)
- else:
- log.debug("unrecognized PROTOCOLINFO line type '%s', ignoring entry: %s" % (line_type, line))
-
- self.auth_methods = tuple(auth_methods)
- self.unknown_auth_methods = tuple(unknown_auth_methods)
-
diff --git a/stem/control.py b/stem/control.py
index ccb57f4..b188ea4 100644
--- a/stem/control.py
+++ b/stem/control.py
@@ -26,6 +26,7 @@ import time
import Queue
import threading
+import stem.response
import stem.socket
import stem.util.log as log
@@ -475,7 +476,7 @@ class Controller(BaseController):
if response.content()[0][0] != "250":
raise stem.socket.ControllerError(str(response))
- GetInfoResponse.convert(response)
+ stem.response.convert("GETINFO", response)
# error if we got back different parameters than we requested
requested_params = set(param)
@@ -495,67 +496,3 @@ class Controller(BaseController):
if default == UNDEFINED: raise exc
else: return default
-class GetInfoResponse(stem.socket.ControlMessage):
- """
- Reply for a GETINFO query.
-
- Attributes:
- values (dict) - mapping between the queried options and their values
- """
-
- def convert(control_message):
- """
- Parses a ControlMessage, performing an in-place conversion of it into a
- GetInfoResponse.
-
- Arguments:
- control_message (stem.socket.ControlMessage) -
- message to be parsed as a GETINFO reply
-
- Raises:
- stem.socket.ProtocolError the message isn't a proper GETINFO response
- TypeError if argument isn't a ControlMessage
- """
-
- if isinstance(control_message, stem.socket.ControlMessage):
- control_message.__class__ = GetInfoResponse
- control_message._parse_message()
- return control_message
- else:
- raise TypeError("Only able to convert stem.socket.ControlMessage instances")
-
- convert = staticmethod(convert)
-
- def _parse_message(self):
- # Example:
- # 250-version=0.2.3.11-alpha-dev (git-ef0bc7f8f26a917c)
- # 250+config-text=
- # ControlPort 9051
- # DataDirectory /home/atagar/.tor
- # ExitPolicy reject *:*
- # Log notice stdout
- # Nickname Unnamed
- # ORPort 9050
- # .
- # 250 OK
-
- self.values = {}
-
- for line in self:
- if line == "OK": break
- elif not "=" in line:
- raise stem.socket.ProtocolError("GETINFO replies should only contain parameter=value mappings: %s" % line)
-
- key, value = line.split("=", 1)
-
- # if the value is a multiline value then it *must* be of the form
- # '<key>=\n<value>'
-
- if "\n" in value:
- if value.startswith("\n"):
- value = value[1:]
- else:
- raise stem.socket.ProtocolError("GETINFO response contained a multiline value that didn't start with a newline: %s" % line)
-
- self.values[key] = value
-
diff --git a/stem/response/__init__.py b/stem/response/__init__.py
new file mode 100644
index 0000000..92b9846
--- /dev/null
+++ b/stem/response/__init__.py
@@ -0,0 +1,46 @@
+"""
+Parses replies from the control socket.
+
+converts - translates a ControlMessage into a particular response subclass
+"""
+
+__all__ = ["getinfo", "protocolinfo"]
+
+import stem.socket
+
+def convert(response_type, message):
+ """
+ Converts a ControlMessage into a particular kind of tor response. This does
+ an in-place conversion of the message from being a ControlMessage to a
+ subclass for its response type. Recognized types include...
+
+ * GETINFO
+ * PROTOCOLINFO
+
+ If the response_type isn't recognized then this is leaves it alone.
+
+ Arguments:
+ response_type (str) - type of tor response to convert to
+ message (stem.socket.ControlMessage) - message to be converted
+
+ Raises:
+ stem.socket.ProtocolError the message isn't a proper response of that type
+ TypeError if argument isn't a ControlMessage or response_type isn't
+ supported
+ """
+
+ import stem.response.getinfo
+ import stem.response.protocolinfo
+
+ if not isinstance(message, stem.socket.ControlMessage):
+ raise TypeError("Only able to convert stem.socket.ControlMessage instances")
+
+ if response_type == "GETINFO":
+ response_class = stem.response.getinfo.GetInfoResponse
+ elif response_type == "PROTOCOLINFO":
+ response_class = stem.response.protocolinfo.ProtocolInfoResponse
+ else: raise TypeError("Unsupported response type: %s" % response_type)
+
+ message.__class__ = response_class
+ message._parse_message()
+
diff --git a/stem/response/getinfo.py b/stem/response/getinfo.py
new file mode 100644
index 0000000..a13a18f
--- /dev/null
+++ b/stem/response/getinfo.py
@@ -0,0 +1,43 @@
+import stem.socket
+
+class GetInfoResponse(stem.socket.ControlMessage):
+ """
+ Reply for a GETINFO query.
+
+ Attributes:
+ values (dict) - mapping between the queried options and their values
+ """
+
+ def _parse_message(self):
+ # Example:
+ # 250-version=0.2.3.11-alpha-dev (git-ef0bc7f8f26a917c)
+ # 250+config-text=
+ # ControlPort 9051
+ # DataDirectory /home/atagar/.tor
+ # ExitPolicy reject *:*
+ # Log notice stdout
+ # Nickname Unnamed
+ # ORPort 9050
+ # .
+ # 250 OK
+
+ self.values = {}
+
+ for line in self:
+ if line == "OK": break
+ elif not "=" in line:
+ raise stem.socket.ProtocolError("GETINFO replies should only contain parameter=value mappings: %s" % line)
+
+ key, value = line.split("=", 1)
+
+ # if the value is a multiline value then it *must* be of the form
+ # '<key>=\n<value>'
+
+ if "\n" in value:
+ if value.startswith("\n"):
+ value = value[1:]
+ else:
+ raise stem.socket.ProtocolError("GETINFO response contained a multiline value that didn't start with a newline: %s" % line)
+
+ self.values[key] = value
+
diff --git a/stem/response/protocolinfo.py b/stem/response/protocolinfo.py
new file mode 100644
index 0000000..4547a1d
--- /dev/null
+++ b/stem/response/protocolinfo.py
@@ -0,0 +1,131 @@
+import stem.connection
+import stem.socket
+import stem.version
+import stem.util.log as log
+
+class ProtocolInfoResponse(stem.socket.ControlMessage):
+ """
+ Version one PROTOCOLINFO query response.
+
+ According to the control spec the cookie_file is an absolute path. However,
+ this often is not the case (especially for the Tor Browser Bundle)...
+ https://trac.torproject.org/projects/tor/ticket/1101
+
+ If the path is relative then we'll make an attempt (which may not work) to
+ correct this.
+
+ The protocol_version is the only mandatory data for a valid PROTOCOLINFO
+ response, so all other values are None if undefined or empty if a collection.
+
+ Attributes:
+ protocol_version (int) - protocol version of the response
+ tor_version (stem.version.Version) - version of the tor process
+ auth_methods (tuple) - AuthMethod types that tor will accept
+ unknown_auth_methods (tuple) - strings of unrecognized auth methods
+ cookie_path (str) - path of tor's authentication cookie
+ """
+
+ def _parse_message(self):
+ # Example:
+ # 250-PROTOCOLINFO 1
+ # 250-AUTH METHODS=COOKIE COOKIEFILE="/home/atagar/.tor/control_auth_cookie"
+ # 250-VERSION Tor="0.2.1.30"
+ # 250 OK
+
+ self.protocol_version = None
+ self.tor_version = None
+ self.cookie_path = None
+
+ auth_methods, unknown_auth_methods = [], []
+
+ # sanity check that we're a PROTOCOLINFO response
+ if not list(self)[0].startswith("PROTOCOLINFO"):
+ msg = "Message is not a PROTOCOLINFO response (%s)" % self
+ raise stem.socket.ProtocolError(msg)
+
+ for line in self:
+ if line == "OK": break
+ elif line.is_empty(): continue # blank line
+
+ line_type = line.pop()
+
+ if line_type == "PROTOCOLINFO":
+ # Line format:
+ # FirstLine = "PROTOCOLINFO" SP PIVERSION CRLF
+ # PIVERSION = 1*DIGIT
+
+ if line.is_empty():
+ msg = "PROTOCOLINFO response's initial line is missing the protocol version: %s" % line
+ raise stem.socket.ProtocolError(msg)
+
+ piversion = line.pop()
+
+ if not piversion.isdigit():
+ msg = "PROTOCOLINFO response version is non-numeric: %s" % line
+ raise stem.socket.ProtocolError(msg)
+
+ self.protocol_version = int(piversion)
+
+ # The piversion really should be "1" but, according to the spec, tor
+ # does not necessarily need to provide the PROTOCOLINFO version that we
+ # requested. Log if it's something we aren't expecting but still make
+ # an effort to parse like a v1 response.
+
+ if self.protocol_version != 1:
+ log.info("We made a PROTOCOLINFO version 1 query but got a version %i response instead. We'll still try to use it, but this may cause problems." % self.protocol_version)
+ elif line_type == "AUTH":
+ # Line format:
+ # AuthLine = "250-AUTH" SP "METHODS=" AuthMethod *("," AuthMethod)
+ # *(SP "COOKIEFILE=" AuthCookieFile) CRLF
+ # AuthMethod = "NULL" / "HASHEDPASSWORD" / "COOKIE"
+ # AuthCookieFile = QuotedString
+
+ # parse AuthMethod mapping
+ if not line.is_next_mapping("METHODS"):
+ msg = "PROTOCOLINFO response's AUTH line is missing its mandatory 'METHODS' mapping: %s" % line
+ raise stem.socket.ProtocolError(msg)
+
+ for method in line.pop_mapping()[1].split(","):
+ if method == "NULL":
+ auth_methods.append(stem.connection.AuthMethod.NONE)
+ elif method == "HASHEDPASSWORD":
+ auth_methods.append(stem.connection.AuthMethod.PASSWORD)
+ elif method == "COOKIE":
+ auth_methods.append(stem.connection.AuthMethod.COOKIE)
+ else:
+ unknown_auth_methods.append(method)
+ message_id = "stem.connection.unknown_auth_%s" % method
+ log.log_once(message_id, log.INFO, "PROTOCOLINFO response included a type of authentication that we don't recognize: %s" % method)
+
+ # our auth_methods should have a single AuthMethod.UNKNOWN entry if
+ # any unknown authentication methods exist
+ if not stem.connection.AuthMethod.UNKNOWN in auth_methods:
+ auth_methods.append(stem.connection.AuthMethod.UNKNOWN)
+
+ # parse optional COOKIEFILE mapping (quoted and can have escapes)
+ if line.is_next_mapping("COOKIEFILE", True, True):
+ self.cookie_path = line.pop_mapping(True, True)[1]
+
+ # attempt to expand relative cookie paths
+ stem.connection._expand_cookie_path(self, stem.util.system.get_pid_by_name, "tor")
+ elif line_type == "VERSION":
+ # Line format:
+ # VersionLine = "250-VERSION" SP "Tor=" TorVersion OptArguments CRLF
+ # TorVersion = QuotedString
+
+ if not line.is_next_mapping("Tor", True):
+ msg = "PROTOCOLINFO response's VERSION line is missing its mandatory tor version mapping: %s" % line
+ raise stem.socket.ProtocolError(msg)
+
+ torversion = line.pop_mapping(True)[1]
+
+ try:
+ self.tor_version = stem.version.Version(torversion)
+ except ValueError, exc:
+ raise stem.socket.ProtocolError(exc)
+ else:
+ log.debug("unrecognized PROTOCOLINFO line type '%s', ignoring entry: %s" % (line_type, line))
+
+ self.auth_methods = tuple(auth_methods)
+ self.unknown_auth_methods = tuple(unknown_auth_methods)
+
diff --git a/test/integ/connection/__init__.py b/test/integ/connection/__init__.py
index 90471c0..eec60e6 100644
--- a/test/integ/connection/__init__.py
+++ b/test/integ/connection/__init__.py
@@ -2,5 +2,5 @@
Integration tests for stem.connection.
"""
-__all__ = ["authenticate", "connect", "protocolinfo"]
+__all__ = ["authenticate", "connect"]
diff --git a/test/integ/connection/protocolinfo.py b/test/integ/connection/protocolinfo.py
deleted file mode 100644
index 728c746..0000000
--- a/test/integ/connection/protocolinfo.py
+++ /dev/null
@@ -1,138 +0,0 @@
-"""
-Integration tests for the stem.connection.ProtocolInfoResponse class and
-related functions.
-"""
-
-import unittest
-
-import test.runner
-import stem.socket
-import stem.connection
-import stem.util.system
-import test.mocking as mocking
-from test.integ.util.system import filter_system_call
-
-class TestProtocolInfo(unittest.TestCase):
- def setUp(self):
- test.runner.require_control(self)
- mocking.mock(stem.util.proc.is_available, mocking.return_false())
- mocking.mock(stem.util.system.is_available, mocking.return_true())
-
- def tearDown(self):
- mocking.revert_mocking()
-
- def test_parsing(self):
- """
- Makes a PROTOCOLINFO query and processes the response for our control
- connection.
- """
-
- control_socket = test.runner.get_runner().get_tor_socket(False)
- control_socket.send("PROTOCOLINFO 1")
- protocolinfo_response = control_socket.recv()
- stem.connection.ProtocolInfoResponse.convert(protocolinfo_response)
- control_socket.close()
-
- # according to the control spec the following _could_ differ or be
- # undefined but if that actually happens then it's gonna make people sad
-
- self.assertEqual(1, protocolinfo_response.protocol_version)
- self.assertNotEqual(None, protocolinfo_response.tor_version)
- self.assertNotEqual(None, protocolinfo_response.auth_methods)
-
- self.assert_matches_test_config(protocolinfo_response)
-
- def test_get_protocolinfo_path_expansion(self):
- """
- If we're running with the 'RELATIVE' target then test_parsing() will
- exercise cookie path expansion when we're able to query the pid by our
- prcess name. This test selectively disables system.call() so we exercise
- the expansion via our control port or socket file.
-
- This test is largely redundant with test_parsing() if we aren't running
- with the 'RELATIVE' target.
- """
-
- if test.runner.Torrc.PORT in test.runner.get_runner().get_options():
- cwd_by_port_lookup_prefixes = (
- stem.util.system.GET_PID_BY_PORT_NETSTAT,
- stem.util.system.GET_PID_BY_PORT_SOCKSTAT % "",
- stem.util.system.GET_PID_BY_PORT_LSOF,
- stem.util.system.GET_CWD_PWDX % "",
- "lsof -a -p ")
-
- mocking.mock(stem.util.system.call, filter_system_call(cwd_by_port_lookup_prefixes))
- control_socket = stem.socket.ControlPort(control_port = test.runner.CONTROL_PORT)
- else:
- cwd_by_socket_lookup_prefixes = (
- stem.util.system.GET_PID_BY_FILE_LSOF % "",
- stem.util.system.GET_CWD_PWDX % "",
- "lsof -a -p ")
-
- mocking.mock(stem.util.system.call, filter_system_call(cwd_by_socket_lookup_prefixes))
- control_socket = stem.socket.ControlSocketFile(test.runner.CONTROL_SOCKET_PATH)
-
- protocolinfo_response = stem.connection.get_protocolinfo(control_socket)
- self.assert_matches_test_config(protocolinfo_response)
-
- # we should have a usable socket at this point
- self.assertTrue(control_socket.is_alive())
- control_socket.close()
-
- def test_multiple_protocolinfo_calls(self):
- """
- Tests making repeated PROTOCOLINFO queries. This use case is interesting
- because tor will shut down the socket and stem should transparently
- re-establish it.
- """
-
- with test.runner.get_runner().get_tor_socket(False) as control_socket:
- for i in range(5):
- protocolinfo_response = stem.connection.get_protocolinfo(control_socket)
- self.assert_matches_test_config(protocolinfo_response)
-
- def test_pre_disconnected_query(self):
- """
- Tests making a PROTOCOLINFO query when previous use of the socket had
- already disconnected it.
- """
-
- with test.runner.get_runner().get_tor_socket(False) as control_socket:
- # makes a couple protocolinfo queries outside of get_protocolinfo first
- control_socket.send("PROTOCOLINFO 1")
- control_socket.recv()
-
- control_socket.send("PROTOCOLINFO 1")
- control_socket.recv()
-
- protocolinfo_response = stem.connection.get_protocolinfo(control_socket)
- self.assert_matches_test_config(protocolinfo_response)
-
- def assert_matches_test_config(self, protocolinfo_response):
- """
- Makes assertions that the protocolinfo response's attributes match those of
- the test configuration.
- """
-
- runner = test.runner.get_runner()
- tor_options = runner.get_options()
- auth_methods, auth_cookie_path = [], None
-
- if test.runner.Torrc.COOKIE in tor_options:
- auth_methods.append(stem.connection.AuthMethod.COOKIE)
- chroot_path = runner.get_chroot()
- auth_cookie_path = runner.get_auth_cookie_path()
-
- if chroot_path and auth_cookie_path.startswith(chroot_path):
- auth_cookie_path = auth_cookie_path[len(chroot_path):]
-
- if test.runner.Torrc.PASSWORD in tor_options:
- auth_methods.append(stem.connection.AuthMethod.PASSWORD)
-
- if not auth_methods:
- auth_methods.append(stem.connection.AuthMethod.NONE)
-
- self.assertEqual((), protocolinfo_response.unknown_auth_methods)
- self.assertEqual(tuple(auth_methods), protocolinfo_response.auth_methods)
- self.assertEqual(auth_cookie_path, protocolinfo_response.cookie_path)
-
diff --git a/test/integ/response/__init__.py b/test/integ/response/__init__.py
new file mode 100644
index 0000000..2e3a991
--- /dev/null
+++ b/test/integ/response/__init__.py
@@ -0,0 +1,6 @@
+"""
+Integration tests for stem.response.
+"""
+
+__all__ = ["protocolinfo"]
+
diff --git a/test/integ/response/protocolinfo.py b/test/integ/response/protocolinfo.py
new file mode 100644
index 0000000..f5eb518
--- /dev/null
+++ b/test/integ/response/protocolinfo.py
@@ -0,0 +1,138 @@
+"""
+Integration tests for the stem.response.protocolinfo.ProtocolInfoResponse class
+and related functions.
+"""
+
+import unittest
+
+import test.runner
+import stem.socket
+import stem.connection
+import stem.util.system
+import test.mocking as mocking
+from test.integ.util.system import filter_system_call
+
+class TestProtocolInfo(unittest.TestCase):
+ def setUp(self):
+ test.runner.require_control(self)
+ mocking.mock(stem.util.proc.is_available, mocking.return_false())
+ mocking.mock(stem.util.system.is_available, mocking.return_true())
+
+ def tearDown(self):
+ mocking.revert_mocking()
+
+ def test_parsing(self):
+ """
+ Makes a PROTOCOLINFO query and processes the response for our control
+ connection.
+ """
+
+ control_socket = test.runner.get_runner().get_tor_socket(False)
+ control_socket.send("PROTOCOLINFO 1")
+ protocolinfo_response = control_socket.recv()
+ stem.response.convert("PROTOCOLINFO", protocolinfo_response)
+ control_socket.close()
+
+ # according to the control spec the following _could_ differ or be
+ # undefined but if that actually happens then it's gonna make people sad
+
+ self.assertEqual(1, protocolinfo_response.protocol_version)
+ self.assertNotEqual(None, protocolinfo_response.tor_version)
+ self.assertNotEqual(None, protocolinfo_response.auth_methods)
+
+ self.assert_matches_test_config(protocolinfo_response)
+
+ def test_get_protocolinfo_path_expansion(self):
+ """
+ If we're running with the 'RELATIVE' target then test_parsing() will
+ exercise cookie path expansion when we're able to query the pid by our
+ prcess name. This test selectively disables system.call() so we exercise
+ the expansion via our control port or socket file.
+
+ This test is largely redundant with test_parsing() if we aren't running
+ with the 'RELATIVE' target.
+ """
+
+ if test.runner.Torrc.PORT in test.runner.get_runner().get_options():
+ cwd_by_port_lookup_prefixes = (
+ stem.util.system.GET_PID_BY_PORT_NETSTAT,
+ stem.util.system.GET_PID_BY_PORT_SOCKSTAT % "",
+ stem.util.system.GET_PID_BY_PORT_LSOF,
+ stem.util.system.GET_CWD_PWDX % "",
+ "lsof -a -p ")
+
+ mocking.mock(stem.util.system.call, filter_system_call(cwd_by_port_lookup_prefixes))
+ control_socket = stem.socket.ControlPort(control_port = test.runner.CONTROL_PORT)
+ else:
+ cwd_by_socket_lookup_prefixes = (
+ stem.util.system.GET_PID_BY_FILE_LSOF % "",
+ stem.util.system.GET_CWD_PWDX % "",
+ "lsof -a -p ")
+
+ mocking.mock(stem.util.system.call, filter_system_call(cwd_by_socket_lookup_prefixes))
+ control_socket = stem.socket.ControlSocketFile(test.runner.CONTROL_SOCKET_PATH)
+
+ protocolinfo_response = stem.connection.get_protocolinfo(control_socket)
+ self.assert_matches_test_config(protocolinfo_response)
+
+ # we should have a usable socket at this point
+ self.assertTrue(control_socket.is_alive())
+ control_socket.close()
+
+ def test_multiple_protocolinfo_calls(self):
+ """
+ Tests making repeated PROTOCOLINFO queries. This use case is interesting
+ because tor will shut down the socket and stem should transparently
+ re-establish it.
+ """
+
+ with test.runner.get_runner().get_tor_socket(False) as control_socket:
+ for i in range(5):
+ protocolinfo_response = stem.connection.get_protocolinfo(control_socket)
+ self.assert_matches_test_config(protocolinfo_response)
+
+ def test_pre_disconnected_query(self):
+ """
+ Tests making a PROTOCOLINFO query when previous use of the socket had
+ already disconnected it.
+ """
+
+ with test.runner.get_runner().get_tor_socket(False) as control_socket:
+ # makes a couple protocolinfo queries outside of get_protocolinfo first
+ control_socket.send("PROTOCOLINFO 1")
+ control_socket.recv()
+
+ control_socket.send("PROTOCOLINFO 1")
+ control_socket.recv()
+
+ protocolinfo_response = stem.connection.get_protocolinfo(control_socket)
+ self.assert_matches_test_config(protocolinfo_response)
+
+ def assert_matches_test_config(self, protocolinfo_response):
+ """
+ Makes assertions that the protocolinfo response's attributes match those of
+ the test configuration.
+ """
+
+ runner = test.runner.get_runner()
+ tor_options = runner.get_options()
+ auth_methods, auth_cookie_path = [], None
+
+ if test.runner.Torrc.COOKIE in tor_options:
+ auth_methods.append(stem.connection.AuthMethod.COOKIE)
+ chroot_path = runner.get_chroot()
+ auth_cookie_path = runner.get_auth_cookie_path()
+
+ if chroot_path and auth_cookie_path.startswith(chroot_path):
+ auth_cookie_path = auth_cookie_path[len(chroot_path):]
+
+ if test.runner.Torrc.PASSWORD in tor_options:
+ auth_methods.append(stem.connection.AuthMethod.PASSWORD)
+
+ if not auth_methods:
+ auth_methods.append(stem.connection.AuthMethod.NONE)
+
+ self.assertEqual((), protocolinfo_response.unknown_auth_methods)
+ self.assertEqual(tuple(auth_methods), protocolinfo_response.auth_methods)
+ self.assertEqual(auth_cookie_path, protocolinfo_response.cookie_path)
+
diff --git a/test/mocking.py b/test/mocking.py
index aa61c76..f17dcc9 100644
--- a/test/mocking.py
+++ b/test/mocking.py
@@ -18,7 +18,7 @@ Mocking Functions
Instance Constructors
get_message - stem.socket.ControlMessage
- get_protocolinfo_response - stem.connection.ProtocolInfoResponse
+ get_protocolinfo_response - stem.response.protocolinfo.ProtocolInfoResponse
"""
import inspect
@@ -26,7 +26,7 @@ import itertools
import StringIO
import __builtin__
-import stem.connection
+import stem.response
import stem.socket
# Once we've mocked a function we can't rely on its __module__ or __name__
@@ -223,11 +223,11 @@ def get_protocolinfo_response(**attributes):
attributes (dict) - attributes to customize the response with
Returns:
- stem.connection.ProtocolInfoResponse instance
+ stem.response.protocolinfo.ProtocolInfoResponse instance
"""
protocolinfo_response = get_message("250-PROTOCOLINFO 1\n250 OK")
- stem.connection.ProtocolInfoResponse.convert(protocolinfo_response)
+ stem.response.convert("PROTOCOLINFO", protocolinfo_response)
for attr in attributes:
protocolinfo_response.__dict__[attr] = attributes[attr]
diff --git a/test/unit/connection/__init__.py b/test/unit/connection/__init__.py
index 4eae0fa..7073319 100644
--- a/test/unit/connection/__init__.py
+++ b/test/unit/connection/__init__.py
@@ -2,5 +2,5 @@
Unit tests for stem.connection.
"""
-__all__ = ["authentication", "protocolinfo"]
+__all__ = ["authentication"]
diff --git a/test/unit/connection/protocolinfo.py b/test/unit/connection/protocolinfo.py
deleted file mode 100644
index 795d780..0000000
--- a/test/unit/connection/protocolinfo.py
+++ /dev/null
@@ -1,178 +0,0 @@
-"""
-Unit tests for the stem.connection.ProtocolInfoResponse class.
-"""
-
-import unittest
-
-import stem.connection
-import stem.socket
-import stem.version
-import stem.util.proc
-import stem.util.system
-import test.mocking as mocking
-
-NO_AUTH = """250-PROTOCOLINFO 1
-250-AUTH METHODS=NULL
-250-VERSION Tor="0.2.1.30"
-250 OK"""
-
-PASSWORD_AUTH = """250-PROTOCOLINFO 1
-250-AUTH METHODS=HASHEDPASSWORD
-250-VERSION Tor="0.2.1.30"
-250 OK"""
-
-COOKIE_AUTH = r"""250-PROTOCOLINFO 1
-250-AUTH METHODS=COOKIE COOKIEFILE="/tmp/my data\\\"dir//control_auth_cookie"
-250-VERSION Tor="0.2.1.30"
-250 OK"""
-
-MULTIPLE_AUTH = """250-PROTOCOLINFO 1
-250-AUTH METHODS=COOKIE,HASHEDPASSWORD COOKIEFILE="/home/atagar/.tor/control_auth_cookie"
-250-VERSION Tor="0.2.1.30"
-250 OK"""
-
-UNKNOWN_AUTH = """250-PROTOCOLINFO 1
-250-AUTH METHODS=MAGIC,HASHEDPASSWORD,PIXIE_DUST
-250-VERSION Tor="0.2.1.30"
-250 OK"""
-
-MINIMUM_RESPONSE = """250-PROTOCOLINFO 5
-250 OK"""
-
-RELATIVE_COOKIE_PATH = r"""250-PROTOCOLINFO 1
-250-AUTH METHODS=COOKIE COOKIEFILE="./tor-browser_en-US/Data/control_auth_cookie"
-250-VERSION Tor="0.2.1.30"
-250 OK"""
-
-class TestProtocolInfoResponse(unittest.TestCase):
- def test_convert(self):
- """
- Exercises functionality of the convert method both when it works and
- there's an error.
- """
-
- # working case
- control_message = mocking.get_message(NO_AUTH)
- stem.connection.ProtocolInfoResponse.convert(control_message)
-
- # now this should be a ProtocolInfoResponse (ControlMessage subclass)
- self.assertTrue(isinstance(control_message, stem.socket.ControlMessage))
- self.assertTrue(isinstance(control_message, stem.connection.ProtocolInfoResponse))
-
- # exercise some of the ControlMessage functionality
- raw_content = (NO_AUTH + "\n").replace("\n", "\r\n")
- self.assertEquals(raw_content, control_message.raw_content())
- self.assertTrue(str(control_message).startswith("PROTOCOLINFO 1"))
-
- # attempt to convert the wrong type
- self.assertRaises(TypeError, stem.connection.ProtocolInfoResponse.convert, "hello world")
-
- # attempt to convert a different message type
- bw_event_control_message = mocking.get_message("650 BW 32326 2856")
- self.assertRaises(stem.socket.ProtocolError, stem.connection.ProtocolInfoResponse.convert, bw_event_control_message)
-
- def test_no_auth(self):
- """
- Checks a response when there's no authentication.
- """
-
- control_message = mocking.get_message(NO_AUTH)
- stem.connection.ProtocolInfoResponse.convert(control_message)
-
- self.assertEquals(1, control_message.protocol_version)
- self.assertEquals(stem.version.Version("0.2.1.30"), control_message.tor_version)
- self.assertEquals((stem.connection.AuthMethod.NONE, ), control_message.auth_methods)
- self.assertEquals((), control_message.unknown_auth_methods)
- self.assertEquals(None, control_message.cookie_path)
-
- def test_password_auth(self):
- """
- Checks a response with password authentication.
- """
-
- control_message = mocking.get_message(PASSWORD_AUTH)
- stem.connection.ProtocolInfoResponse.convert(control_message)
- self.assertEquals((stem.connection.AuthMethod.PASSWORD, ), control_message.auth_methods)
-
- def test_cookie_auth(self):
- """
- Checks a response with cookie authentication and a path including escape
- characters.
- """
-
- control_message = mocking.get_message(COOKIE_AUTH)
- stem.connection.ProtocolInfoResponse.convert(control_message)
- self.assertEquals((stem.connection.AuthMethod.COOKIE, ), control_message.auth_methods)
- self.assertEquals("/tmp/my data\\\"dir//control_auth_cookie", control_message.cookie_path)
-
- def test_multiple_auth(self):
- """
- Checks a response with multiple authentication methods.
- """
-
- control_message = mocking.get_message(MULTIPLE_AUTH)
- stem.connection.ProtocolInfoResponse.convert(control_message)
- self.assertEquals((stem.connection.AuthMethod.COOKIE, stem.connection.AuthMethod.PASSWORD), control_message.auth_methods)
- self.assertEquals("/home/atagar/.tor/control_auth_cookie", control_message.cookie_path)
-
- def test_unknown_auth(self):
- """
- Checks a response with an unrecognized authtentication method.
- """
-
- control_message = mocking.get_message(UNKNOWN_AUTH)
- stem.connection.ProtocolInfoResponse.convert(control_message)
- self.assertEquals((stem.connection.AuthMethod.UNKNOWN, stem.connection.AuthMethod.PASSWORD), control_message.auth_methods)
- self.assertEquals(("MAGIC", "PIXIE_DUST"), control_message.unknown_auth_methods)
-
- def test_minimum_response(self):
- """
- Checks a PROTOCOLINFO response that only contains the minimum amount of
- information to be a valid response.
- """
-
- control_message = mocking.get_message(MINIMUM_RESPONSE)
- stem.connection.ProtocolInfoResponse.convert(control_message)
-
- self.assertEquals(5, control_message.protocol_version)
- self.assertEquals(None , control_message.tor_version)
- self.assertEquals((), control_message.auth_methods)
- self.assertEquals((), control_message.unknown_auth_methods)
- self.assertEquals(None, control_message.cookie_path)
-
- def test_relative_cookie(self):
- """
- Checks an authentication cookie with a relative path where expansion both
- succeeds and fails.
- """
-
- # we need to mock both pid and cwd lookups since the general cookie
- # expanion works by...
- # - resolving the pid of the "tor" process
- # - using that to get tor's cwd
-
- def call_mocking(command):
- if command == stem.util.system.GET_PID_BY_NAME_PGREP % "tor":
- return ["10"]
- elif command == stem.util.system.GET_CWD_PWDX % 10:
- return ["10: /tmp/foo"]
-
- mocking.mock(stem.util.proc.is_available, mocking.return_false())
- mocking.mock(stem.util.system.is_available, mocking.return_true())
- mocking.mock(stem.util.system.call, call_mocking)
-
- control_message = mocking.get_message(RELATIVE_COOKIE_PATH)
- stem.connection.ProtocolInfoResponse.convert(control_message)
- self.assertEquals("/tmp/foo/tor-browser_en-US/Data/control_auth_cookie", control_message.cookie_path)
-
- # exercise cookie expansion where both calls fail (should work, just
- # leaving the path unexpanded)
-
- mocking.mock(stem.util.system.call, mocking.return_none())
- control_message = mocking.get_message(RELATIVE_COOKIE_PATH)
- stem.connection.ProtocolInfoResponse.convert(control_message)
- self.assertEquals("./tor-browser_en-US/Data/control_auth_cookie", control_message.cookie_path)
-
- # reset system call mocking
- mocking.revert_mocking()
-
diff --git a/test/unit/control/getinfo.py b/test/unit/control/getinfo.py
deleted file mode 100644
index 3fc9fd3..0000000
--- a/test/unit/control/getinfo.py
+++ /dev/null
@@ -1,119 +0,0 @@
-"""
-Unit tests for the stem.control.GetInfoResponse class.
-"""
-
-import unittest
-
-import stem.connection
-import test.mocking as mocking
-
-EMPTY_RESPONSE = "250 OK"
-
-SINGLE_RESPONSE = """\
-250-version=0.2.3.11-alpha-dev
-250 OK"""
-
-BATCH_RESPONSE = """\
-250-version=0.2.3.11-alpha-dev
-250-address=67.137.76.214
-250-fingerprint=5FDE0422045DF0E1879A3738D09099EB4A0C5BA0
-250 OK"""
-
-MULTILINE_RESPONSE = """\
-250-version=0.2.3.11-alpha-dev (git-ef0bc7f8f26a917c)
-250+config-text=
-ControlPort 9051
-DataDirectory /home/atagar/.tor
-ExitPolicy reject *:*
-Log notice stdout
-Nickname Unnamed
-ORPort 9050
-.
-250 OK"""
-
-NON_KEY_VALUE_ENTRY = """\
-250-version=0.2.3.11-alpha-dev
-250-address 67.137.76.214
-250 OK"""
-
-MISSING_MULTILINE_NEWLINE = """\
-250+config-text=ControlPort 9051
-DataDirectory /home/atagar/.tor
-.
-250 OK"""
-
-class TestGetInfoResponse(unittest.TestCase):
- def test_empty_response(self):
- """
- Parses a GETINFO reply without options (just calling "GETINFO").
- """
-
- control_message = mocking.get_message(EMPTY_RESPONSE)
- stem.control.GetInfoResponse.convert(control_message)
-
- # now this should be a GetInfoResponse (ControlMessage subclass)
- self.assertTrue(isinstance(control_message, stem.socket.ControlMessage))
- self.assertTrue(isinstance(control_message, stem.control.GetInfoResponse))
-
- self.assertEqual({}, control_message.values)
-
- def test_single_response(self):
- """
- Parses a GETINFO reply response for a single parameter.
- """
-
- control_message = mocking.get_message(SINGLE_RESPONSE)
- stem.control.GetInfoResponse.convert(control_message)
- self.assertEqual({"version": "0.2.3.11-alpha-dev"}, control_message.values)
-
- def test_batch_response(self):
- """
- Parses a GETINFO reply for muiltiple parameters.
- """
-
- control_message = mocking.get_message(BATCH_RESPONSE)
- stem.control.GetInfoResponse.convert(control_message)
-
- expected = {
- "version": "0.2.3.11-alpha-dev",
- "address": "67.137.76.214",
- "fingerprint": "5FDE0422045DF0E1879A3738D09099EB4A0C5BA0",
- }
-
- self.assertEqual(expected, control_message.values)
-
- def test_multiline_response(self):
- """
- Parses a GETINFO reply for multiple parameters including a multi-line
- value.
- """
-
- control_message = mocking.get_message(MULTILINE_RESPONSE)
- stem.control.GetInfoResponse.convert(control_message)
-
- expected = {
- "version": "0.2.3.11-alpha-dev (git-ef0bc7f8f26a917c)",
- "config-text": "\n".join(MULTILINE_RESPONSE.splitlines()[2:8]),
- }
-
- self.assertEqual(expected, control_message.values)
-
- def test_invalid_non_mapping_content(self):
- """
- Parses a malformed GETINFO reply containing a line that isn't a key=value
- entry.
- """
-
- control_message = mocking.get_message(NON_KEY_VALUE_ENTRY)
- self.assertRaises(stem.socket.ProtocolError, stem.control.GetInfoResponse.convert, control_message)
-
- def test_invalid_multiline_content(self):
- """
- Parses a malformed GETINFO reply with a multi-line entry missing a newline
- between its key and value. This is a proper controller message, but
- malformed according to the GETINFO's spec.
- """
-
- control_message = mocking.get_message(MISSING_MULTILINE_NEWLINE)
- self.assertRaises(stem.socket.ProtocolError, stem.control.GetInfoResponse.convert, control_message)
-
diff --git a/test/unit/response/__init__.py b/test/unit/response/__init__.py
new file mode 100644
index 0000000..530274c
--- /dev/null
+++ b/test/unit/response/__init__.py
@@ -0,0 +1,6 @@
+"""
+Unit tests for stem.response.
+"""
+
+__all__ = ["getinfo", "protocolinfo"]
+
diff --git a/test/unit/response/getinfo.py b/test/unit/response/getinfo.py
new file mode 100644
index 0000000..5f5862a
--- /dev/null
+++ b/test/unit/response/getinfo.py
@@ -0,0 +1,120 @@
+"""
+Unit tests for the stem.response.getinfo.GetInfoResponse class.
+"""
+
+import unittest
+
+import stem.response
+import stem.response.getinfo
+import test.mocking as mocking
+
+EMPTY_RESPONSE = "250 OK"
+
+SINGLE_RESPONSE = """\
+250-version=0.2.3.11-alpha-dev
+250 OK"""
+
+BATCH_RESPONSE = """\
+250-version=0.2.3.11-alpha-dev
+250-address=67.137.76.214
+250-fingerprint=5FDE0422045DF0E1879A3738D09099EB4A0C5BA0
+250 OK"""
+
+MULTILINE_RESPONSE = """\
+250-version=0.2.3.11-alpha-dev (git-ef0bc7f8f26a917c)
+250+config-text=
+ControlPort 9051
+DataDirectory /home/atagar/.tor
+ExitPolicy reject *:*
+Log notice stdout
+Nickname Unnamed
+ORPort 9050
+.
+250 OK"""
+
+NON_KEY_VALUE_ENTRY = """\
+250-version=0.2.3.11-alpha-dev
+250-address 67.137.76.214
+250 OK"""
+
+MISSING_MULTILINE_NEWLINE = """\
+250+config-text=ControlPort 9051
+DataDirectory /home/atagar/.tor
+.
+250 OK"""
+
+class TestGetInfoResponse(unittest.TestCase):
+ def test_empty_response(self):
+ """
+ Parses a GETINFO reply without options (just calling "GETINFO").
+ """
+
+ control_message = mocking.get_message(EMPTY_RESPONSE)
+ stem.response.convert("GETINFO", control_message)
+
+ # now this should be a GetInfoResponse (ControlMessage subclass)
+ self.assertTrue(isinstance(control_message, stem.socket.ControlMessage))
+ self.assertTrue(isinstance(control_message, stem.response.getinfo.GetInfoResponse))
+
+ self.assertEqual({}, control_message.values)
+
+ def test_single_response(self):
+ """
+ Parses a GETINFO reply response for a single parameter.
+ """
+
+ control_message = mocking.get_message(SINGLE_RESPONSE)
+ stem.response.convert("GETINFO", control_message)
+ self.assertEqual({"version": "0.2.3.11-alpha-dev"}, control_message.values)
+
+ def test_batch_response(self):
+ """
+ Parses a GETINFO reply for muiltiple parameters.
+ """
+
+ control_message = mocking.get_message(BATCH_RESPONSE)
+ stem.response.convert("GETINFO", control_message)
+
+ expected = {
+ "version": "0.2.3.11-alpha-dev",
+ "address": "67.137.76.214",
+ "fingerprint": "5FDE0422045DF0E1879A3738D09099EB4A0C5BA0",
+ }
+
+ self.assertEqual(expected, control_message.values)
+
+ def test_multiline_response(self):
+ """
+ Parses a GETINFO reply for multiple parameters including a multi-line
+ value.
+ """
+
+ control_message = mocking.get_message(MULTILINE_RESPONSE)
+ stem.response.convert("GETINFO", control_message)
+
+ expected = {
+ "version": "0.2.3.11-alpha-dev (git-ef0bc7f8f26a917c)",
+ "config-text": "\n".join(MULTILINE_RESPONSE.splitlines()[2:8]),
+ }
+
+ self.assertEqual(expected, control_message.values)
+
+ def test_invalid_non_mapping_content(self):
+ """
+ Parses a malformed GETINFO reply containing a line that isn't a key=value
+ entry.
+ """
+
+ control_message = mocking.get_message(NON_KEY_VALUE_ENTRY)
+ self.assertRaises(stem.socket.ProtocolError, stem.response.convert, "GETINFO", control_message)
+
+ def test_invalid_multiline_content(self):
+ """
+ Parses a malformed GETINFO reply with a multi-line entry missing a newline
+ between its key and value. This is a proper controller message, but
+ malformed according to the GETINFO's spec.
+ """
+
+ control_message = mocking.get_message(MISSING_MULTILINE_NEWLINE)
+ self.assertRaises(stem.socket.ProtocolError, stem.response.convert, "GETINFO", control_message)
+
diff --git a/test/unit/response/protocolinfo.py b/test/unit/response/protocolinfo.py
new file mode 100644
index 0000000..a17c58c
--- /dev/null
+++ b/test/unit/response/protocolinfo.py
@@ -0,0 +1,180 @@
+"""
+Unit tests for the stem.response.protocolinfo.ProtocolInfoResponse class.
+"""
+
+import unittest
+
+from stem.connection import AuthMethod
+import stem.socket
+import stem.version
+import stem.util.proc
+import stem.util.system
+import stem.response
+import stem.response.protocolinfo
+import test.mocking as mocking
+
+NO_AUTH = """250-PROTOCOLINFO 1
+250-AUTH METHODS=NULL
+250-VERSION Tor="0.2.1.30"
+250 OK"""
+
+PASSWORD_AUTH = """250-PROTOCOLINFO 1
+250-AUTH METHODS=HASHEDPASSWORD
+250-VERSION Tor="0.2.1.30"
+250 OK"""
+
+COOKIE_AUTH = r"""250-PROTOCOLINFO 1
+250-AUTH METHODS=COOKIE COOKIEFILE="/tmp/my data\\\"dir//control_auth_cookie"
+250-VERSION Tor="0.2.1.30"
+250 OK"""
+
+MULTIPLE_AUTH = """250-PROTOCOLINFO 1
+250-AUTH METHODS=COOKIE,HASHEDPASSWORD COOKIEFILE="/home/atagar/.tor/control_auth_cookie"
+250-VERSION Tor="0.2.1.30"
+250 OK"""
+
+UNKNOWN_AUTH = """250-PROTOCOLINFO 1
+250-AUTH METHODS=MAGIC,HASHEDPASSWORD,PIXIE_DUST
+250-VERSION Tor="0.2.1.30"
+250 OK"""
+
+MINIMUM_RESPONSE = """250-PROTOCOLINFO 5
+250 OK"""
+
+RELATIVE_COOKIE_PATH = r"""250-PROTOCOLINFO 1
+250-AUTH METHODS=COOKIE COOKIEFILE="./tor-browser_en-US/Data/control_auth_cookie"
+250-VERSION Tor="0.2.1.30"
+250 OK"""
+
+class TestProtocolInfoResponse(unittest.TestCase):
+ def test_convert(self):
+ """
+ Exercises functionality of the convert method both when it works and
+ there's an error.
+ """
+
+ # working case
+ control_message = mocking.get_message(NO_AUTH)
+ stem.response.convert("PROTOCOLINFO", control_message)
+
+ # now this should be a ProtocolInfoResponse (ControlMessage subclass)
+ self.assertTrue(isinstance(control_message, stem.socket.ControlMessage))
+ self.assertTrue(isinstance(control_message, stem.response.protocolinfo.ProtocolInfoResponse))
+
+ # exercise some of the ControlMessage functionality
+ raw_content = (NO_AUTH + "\n").replace("\n", "\r\n")
+ self.assertEquals(raw_content, control_message.raw_content())
+ self.assertTrue(str(control_message).startswith("PROTOCOLINFO 1"))
+
+ # attempt to convert the wrong type
+ self.assertRaises(TypeError, stem.response.convert, "PROTOCOLINFO", "hello world")
+
+ # attempt to convert a different message type
+ bw_event_control_message = mocking.get_message("650 BW 32326 2856")
+ self.assertRaises(stem.socket.ProtocolError, stem.response.convert, "PROTOCOLINFO", bw_event_control_message)
+
+ def test_no_auth(self):
+ """
+ Checks a response when there's no authentication.
+ """
+
+ control_message = mocking.get_message(NO_AUTH)
+ stem.response.convert("PROTOCOLINFO", control_message)
+
+ self.assertEquals(1, control_message.protocol_version)
+ self.assertEquals(stem.version.Version("0.2.1.30"), control_message.tor_version)
+ self.assertEquals((AuthMethod.NONE, ), control_message.auth_methods)
+ self.assertEquals((), control_message.unknown_auth_methods)
+ self.assertEquals(None, control_message.cookie_path)
+
+ def test_password_auth(self):
+ """
+ Checks a response with password authentication.
+ """
+
+ control_message = mocking.get_message(PASSWORD_AUTH)
+ stem.response.convert("PROTOCOLINFO", control_message)
+ self.assertEquals((AuthMethod.PASSWORD, ), control_message.auth_methods)
+
+ def test_cookie_auth(self):
+ """
+ Checks a response with cookie authentication and a path including escape
+ characters.
+ """
+
+ control_message = mocking.get_message(COOKIE_AUTH)
+ stem.response.convert("PROTOCOLINFO", control_message)
+ self.assertEquals((AuthMethod.COOKIE, ), control_message.auth_methods)
+ self.assertEquals("/tmp/my data\\\"dir//control_auth_cookie", control_message.cookie_path)
+
+ def test_multiple_auth(self):
+ """
+ Checks a response with multiple authentication methods.
+ """
+
+ control_message = mocking.get_message(MULTIPLE_AUTH)
+ stem.response.convert("PROTOCOLINFO", control_message)
+ self.assertEquals((AuthMethod.COOKIE, AuthMethod.PASSWORD), control_message.auth_methods)
+ self.assertEquals("/home/atagar/.tor/control_auth_cookie", control_message.cookie_path)
+
+ def test_unknown_auth(self):
+ """
+ Checks a response with an unrecognized authtentication method.
+ """
+
+ control_message = mocking.get_message(UNKNOWN_AUTH)
+ stem.response.convert("PROTOCOLINFO", control_message)
+ self.assertEquals((AuthMethod.UNKNOWN, AuthMethod.PASSWORD), control_message.auth_methods)
+ self.assertEquals(("MAGIC", "PIXIE_DUST"), control_message.unknown_auth_methods)
+
+ def test_minimum_response(self):
+ """
+ Checks a PROTOCOLINFO response that only contains the minimum amount of
+ information to be a valid response.
+ """
+
+ control_message = mocking.get_message(MINIMUM_RESPONSE)
+ stem.response.convert("PROTOCOLINFO", control_message)
+
+ self.assertEquals(5, control_message.protocol_version)
+ self.assertEquals(None , control_message.tor_version)
+ self.assertEquals((), control_message.auth_methods)
+ self.assertEquals((), control_message.unknown_auth_methods)
+ self.assertEquals(None, control_message.cookie_path)
+
+ def test_relative_cookie(self):
+ """
+ Checks an authentication cookie with a relative path where expansion both
+ succeeds and fails.
+ """
+
+ # we need to mock both pid and cwd lookups since the general cookie
+ # expanion works by...
+ # - resolving the pid of the "tor" process
+ # - using that to get tor's cwd
+
+ def call_mocking(command):
+ if command == stem.util.system.GET_PID_BY_NAME_PGREP % "tor":
+ return ["10"]
+ elif command == stem.util.system.GET_CWD_PWDX % 10:
+ return ["10: /tmp/foo"]
+
+ mocking.mock(stem.util.proc.is_available, mocking.return_false())
+ mocking.mock(stem.util.system.is_available, mocking.return_true())
+ mocking.mock(stem.util.system.call, call_mocking)
+
+ control_message = mocking.get_message(RELATIVE_COOKIE_PATH)
+ stem.response.convert("PROTOCOLINFO", control_message)
+ self.assertEquals("/tmp/foo/tor-browser_en-US/Data/control_auth_cookie", control_message.cookie_path)
+
+ # exercise cookie expansion where both calls fail (should work, just
+ # leaving the path unexpanded)
+
+ mocking.mock(stem.util.system.call, mocking.return_none())
+ control_message = mocking.get_message(RELATIVE_COOKIE_PATH)
+ stem.response.convert("PROTOCOLINFO", control_message)
+ self.assertEquals("./tor-browser_en-US/Data/control_auth_cookie", control_message.cookie_path)
+
+ # reset system call mocking
+ mocking.revert_mocking()
+
_______________________________________________
tor-commits mailing list
tor-commits@xxxxxxxxxxxxxxxxxxxx
https://lists.torproject.org/cgi-bin/mailman/listinfo/tor-commits