[tor-commits] [stem/master] Moving tor response classes into their own module

atagar at torproject.org atagar at torproject.org
Tue May 29 01:08:25 UTC 2012


commit 1d7ab654ec473a5e54ea3253f82abdcadc93158f
Author: Damian Johnson <atagar at torproject.org>
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()
+





More information about the tor-commits mailing list