[tor-commits] [stem/master] RouterStatusEntry unit tests
atagar at torproject.org
atagar at torproject.org
Sat Oct 13 18:35:45 UTC 2012
commit 239d9642bfc800b4f720880f359cdc92a713e63f
Author: Damian Johnson <atagar at torproject.org>
Date: Tue Aug 21 16:52:56 2012 -0700
RouterStatusEntry unit tests
Unit tests for the RouterStatusEntry use cases that come to mind. As normal
they uncovered some bugs with the class.
---
stem/descriptor/networkstatus.py | 44 +++-
test/unit/descriptor/networkstatus.py | 354 +++++++++++++++++++++++++++++++++
2 files changed, 386 insertions(+), 12 deletions(-)
diff --git a/stem/descriptor/networkstatus.py b/stem/descriptor/networkstatus.py
index 96f10ce..021ac4c 100644
--- a/stem/descriptor/networkstatus.py
+++ b/stem/descriptor/networkstatus.py
@@ -531,7 +531,7 @@ class RouterStatusEntry(stem.descriptor.Descriptor):
r_comp = value.split(" ")
- if len(r_comp) < 5:
+ if len(r_comp) < 8:
if not validate: continue
raise ValueError("Router status entry's 'r' line line must have eight values: %s" % line)
@@ -564,7 +564,17 @@ class RouterStatusEntry(stem.descriptor.Descriptor):
# "s" Flags
# s Named Running Stable Valid
- self.flags = value.split(" ")
+ if value == "":
+ self.flags = []
+ else:
+ self.flags = value.split(" ")
+
+ if validate:
+ for flag in self.flags:
+ if self.flags.count(flag) > 1:
+ raise ValueError("Router status entry had duplicate flags: %s" % line)
+ elif flag == "":
+ raise ValueError("Router status entry had extra whitespace on its 's' line: %s" % line)
elif keyword == 'v':
# "v" version
# v Tor 0.2.2.35
@@ -595,16 +605,19 @@ class RouterStatusEntry(stem.descriptor.Descriptor):
raise ValueError("Router status entry's 'w' line needs to start with a 'Bandwidth=' entry: %s" % line)
for w_entry in w_comp:
- w_key, w_value = w_entry.split('=', 1)
+ if '=' in w_entry:
+ w_key, w_value = w_entry.split('=', 1)
+ else:
+ w_key, w_value = w_entry, None
if w_key == "Bandwidth":
- if not w_value.isdigit():
+ if not (w_value and w_value.isdigit()):
if not validate: continue
raise ValueError("Router status entry's 'Bandwidth=' entry needs to have a numeric value: %s" % line)
self.bandwidth = int(w_value)
elif w_key == "Measured":
- if not w_value.isdigit():
+ if not (w_value and w_value.isdigit()):
if not validate: continue
raise ValueError("Router status entry's 'Measured=' entry needs to have a numeric value: %s" % line)
@@ -627,9 +640,11 @@ class RouterStatusEntry(stem.descriptor.Descriptor):
m_comp = value.split(" ")
- if self.document.vote_status != "vote":
+ if not (self.document and self.document.vote_status == "vote"):
if not validate: continue
- raise ValueError("Router status entry's 'm' line should only appear in votes (appeared in a %s): %s" % (self.document.vote_status, line))
+
+ vote_status = self.document.vote_status if self.document else "<undefined document>"
+ raise ValueError("Router status entry's 'm' line should only appear in votes (appeared in a %s): %s" % (vote_status, line))
elif len(m_comp) < 1:
if not validate: continue
raise ValueError("Router status entry's 'm' line needs to start with a series of methods: %s" % line)
@@ -884,7 +899,14 @@ def _decode_fingerprint(identity, validate):
identity += "=" * missing_padding
fingerprint = ""
- for char in base64.b64decode(identity):
+
+ try:
+ identity_decoded = base64.b64decode(identity)
+ except TypeError, exc:
+ if not validate: return None
+ raise ValueError("Unable to decode identity string '%s'" % identity)
+
+ for char in identity_decoded:
# Individual characters are either standard ascii or hex encoded, and each
# represent two hex digits. For instnace...
#
@@ -898,10 +920,8 @@ def _decode_fingerprint(identity, validate):
fingerprint += hex(ord(char))[2:].zfill(2).upper()
if not stem.util.tor_tools.is_valid_fingerprint(fingerprint):
- if validate:
- raise ValueError("Decoded '%s' to be '%s', which isn't a valid fingerprint" % (identity, fingerprint))
- else:
- return None
+ if not validate: return None
+ raise ValueError("Decoded '%s' to be '%s', which isn't a valid fingerprint" % (identity, fingerprint))
return fingerprint
diff --git a/test/unit/descriptor/networkstatus.py b/test/unit/descriptor/networkstatus.py
index e6a8514..c7672e6 100644
--- a/test/unit/descriptor/networkstatus.py
+++ b/test/unit/descriptor/networkstatus.py
@@ -6,6 +6,8 @@ import datetime
import unittest
from stem.descriptor.networkstatus import Flag, RouterStatusEntry, _decode_fingerprint
+from stem.version import Version
+from stem.exit_policy import MicrodescriptorExitPolicy
ROUTER_STATUS_ENTRY_ATTR = (
("r", "caerSidi p1aag7VwarGxqctS7/fS0y5FU+s oQZFLYe9e4A7bOkWKR7TaNxb0JE 2012-08-06 11:19:31 71.35.150.29 9001 0"),
@@ -90,4 +92,356 @@ class TestNetworkStatus(unittest.TestCase):
self.assertEqual(None, entry.exit_policy)
self.assertEqual(None, entry.microdescriptor_hashes)
self.assertEqual([], entry.get_unrecognized_lines())
+
+ def test_rse_missing_fields(self):
+ """
+ Parses a router status entry that's missing fields.
+ """
+
+ content = get_router_status_entry(exclude = ('r', 's'))
+ self._expect_invalid_rse_attr(content, "address")
+
+ content = get_router_status_entry(exclude = ('r',))
+ self._expect_invalid_rse_attr(content, "address")
+
+ content = get_router_status_entry(exclude = ('s',))
+ self._expect_invalid_rse_attr(content, "flags")
+
+ def test_rse_unrecognized_lines(self):
+ """
+ Parses a router status entry with new keywords.
+ """
+
+ content = get_router_status_entry({'z': 'New tor feature: sparkly unicorns!'})
+ entry = RouterStatusEntry(content, None)
+ self.assertEquals(['z New tor feature: sparkly unicorns!'], entry.get_unrecognized_lines())
+
+ def test_rse_proceeding_line(self):
+ """
+ Includes content prior to the 'r' line.
+ """
+
+ content = 'z some stuff\n' + get_router_status_entry()
+ self._expect_invalid_rse_attr(content, "_unrecognized_lines", ['z some stuff'])
+
+ def test_rse_blank_lines(self):
+ """
+ Includes blank lines, which should be ignored.
+ """
+
+ content = get_router_status_entry() + "\n\nv Tor 0.2.2.35\n\n"
+ entry = RouterStatusEntry(content, None)
+ self.assertEqual("Tor 0.2.2.35", entry.version_line)
+
+ def test_rse_missing_r_field(self):
+ """
+ Excludes fields from the 'r' line.
+ """
+
+ components = (
+ ('nickname', 'caerSidi'),
+ ('fingerprint', 'p1aag7VwarGxqctS7/fS0y5FU+s'),
+ ('digest', 'oQZFLYe9e4A7bOkWKR7TaNxb0JE'),
+ ('published', '2012-08-06 11:19:31'),
+ ('address', '71.35.150.29'),
+ ('or_port', '9001'),
+ ('dir_port', '0'),
+ )
+
+ for attr, value in components:
+ # construct the 'r' line without this field
+ test_components = [comp[1] for comp in components]
+ test_components.remove(value)
+ r_line = ' '.join(test_components)
+
+ content = get_router_status_entry({'r': r_line})
+ self._expect_invalid_rse_attr(content, attr)
+
+ def test_rse_malformed_nickname(self):
+ """
+ Parses an 'r' line with a malformed nickname.
+ """
+
+ test_values = (
+ "",
+ "saberrider2008ReallyLongNickname", # too long
+ "$aberrider2008", # invalid characters
+ )
+
+ for value in test_values:
+ r_line = ROUTER_STATUS_ENTRY_ATTR[0][1].replace("caerSidi", value)
+ content = get_router_status_entry({'r': r_line})
+
+ # TODO: Initial whitespace is consumed as part of the keyword/value
+ # divider. This is a bug in the case of V3 router status entries, but
+ # proper behavior for V2 router status entries and server/extrainfo
+ # descriptors.
+ #
+ # I'm inclined to leave this as-is for the moment since fixing it
+ # requires special KEYWORD_LINE handling, and the only result of this bug
+ # is that our validation doesn't catch the new SP restriction on V3
+ # entries.
+
+ if value == "": value = None
+
+ self._expect_invalid_rse_attr(content, "nickname", value)
+
+ def test_rse_malformed_fingerprint(self):
+ """
+ Parses an 'r' line with a malformed fingerprint.
+ """
+
+ test_values = (
+ "",
+ "zzzzz",
+ "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz",
+ )
+
+ for value in test_values:
+ r_line = ROUTER_STATUS_ENTRY_ATTR[0][1].replace("p1aag7VwarGxqctS7/fS0y5FU+s", value)
+ content = get_router_status_entry({'r': r_line})
+ self._expect_invalid_rse_attr(content, "fingerprint")
+
+ def test_rse_malformed_published_date(self):
+ """
+ Parses an 'r' line with a malformed published date.
+ """
+
+ test_values = (
+ "",
+ "2012-08-06 11:19:",
+ "2012-08-06 11:19:71",
+ "2012-08-06 11::31",
+ "2012-08-06 11:79:31",
+ "2012-08-06 :19:31",
+ "2012-08-06 41:19:31",
+ "2012-08- 11:19:31",
+ "2012-08-86 11:19:31",
+ "2012--06 11:19:31",
+ "2012-38-06 11:19:31",
+ "-08-06 11:19:31",
+ "2012-08-06 11:19:31",
+ )
+
+ for value in test_values:
+ r_line = ROUTER_STATUS_ENTRY_ATTR[0][1].replace("2012-08-06 11:19:31", value)
+ content = get_router_status_entry({'r': r_line})
+ self._expect_invalid_rse_attr(content, "published")
+
+ def test_rse_malformed_address(self):
+ """
+ Parses an 'r' line with a malformed address.
+ """
+
+ test_values = (
+ "",
+ "71.35.150.",
+ "71.35..29",
+ "71.35.150",
+ "71.35.150.256",
+ )
+
+ for value in test_values:
+ r_line = ROUTER_STATUS_ENTRY_ATTR[0][1].replace("71.35.150.29", value)
+ content = get_router_status_entry({'r': r_line})
+ self._expect_invalid_rse_attr(content, "address", value)
+
+ def test_rse_malformed_port(self):
+ """
+ Parses an 'r' line with a malformed ORPort or DirPort.
+ """
+
+ test_values = (
+ "",
+ "-1",
+ "399482",
+ "blarg",
+ )
+
+ for value in test_values:
+ for include_or_port in (False, True):
+ for include_dir_port in (False, True):
+ if not include_or_port and not include_dir_port:
+ continue
+
+ r_line = ROUTER_STATUS_ENTRY_ATTR[0][1]
+
+ if include_or_port:
+ r_line = r_line.replace(" 9001 ", " %s " % value)
+
+ if include_dir_port:
+ r_line = r_line[:-1] + value
+
+ attr = "or_port" if include_or_port else "dir_port"
+ expected = int(value) if value.isdigit() else None
+
+ content = get_router_status_entry({'r': r_line})
+ self._expect_invalid_rse_attr(content, attr, expected)
+
+ def test_rse_flags(self):
+ """
+ Handles a variety of flag inputs.
+ """
+
+ test_values = {
+ "": [],
+ "Fast": [Flag.FAST],
+ "Fast Valid": [Flag.FAST, Flag.VALID],
+ "Ugabuga": ["Ugabuga"],
+ }
+
+ for s_line, expected in test_values.items():
+ content = get_router_status_entry({'s': s_line})
+ entry = RouterStatusEntry(content, None)
+ self.assertEquals(expected, entry.flags)
+
+ # tries some invalid inputs
+ test_values = {
+ "Fast ": [Flag.FAST, "", "", ""],
+ "Fast Valid": [Flag.FAST, "", Flag.VALID],
+ "Fast Fast": [Flag.FAST, Flag.FAST],
+ }
+
+ for s_line, expected in test_values.items():
+ content = get_router_status_entry({'s': s_line})
+ self._expect_invalid_rse_attr(content, "flags", expected)
+
+ def test_rse_versions(self):
+ """
+ Handles a variety of version inputs.
+ """
+
+ test_values = {
+ "Tor 0.2.2.35": Version("0.2.2.35"),
+ "Tor 0.1.2": Version("0.1.2"),
+ "Torr new_stuff": None,
+ "new_stuff and stuff": None,
+ }
+
+ for v_line, expected in test_values.items():
+ content = get_router_status_entry({'v': v_line})
+ entry = RouterStatusEntry(content, None)
+ self.assertEquals(expected, entry.version)
+ self.assertEquals(v_line, entry.version_line)
+
+ # tries an invalid input
+ content = get_router_status_entry({'v': "Tor ugabuga"})
+ self._expect_invalid_rse_attr(content, "version")
+
+ def test_rse_bandwidth(self):
+ """
+ Handles a variety of 'w' lines.
+ """
+
+ test_values = {
+ "Bandwidth=0": (0, None, []),
+ "Bandwidth=63138": (63138, None, []),
+ "Bandwidth=11111 Measured=482": (11111, 482, []),
+ "Bandwidth=11111 Measured=482 Blarg!": (11111, 482, ["Blarg!"]),
+ }
+
+ for w_line, expected in test_values.items():
+ content = get_router_status_entry({'w': w_line})
+ entry = RouterStatusEntry(content, None)
+ self.assertEquals(expected[0], entry.bandwidth)
+ self.assertEquals(expected[1], entry.measured)
+ self.assertEquals(expected[2], entry.unrecognized_bandwidth_entries)
+
+ # tries some invalid inputs
+ test_values = (
+ "",
+ "blarg",
+ "Bandwidth",
+ "Bandwidth=",
+ "Bandwidth:0",
+ "Bandwidth 0",
+ "Bandwidth=-10",
+ "Bandwidth=10 Measured",
+ "Bandwidth=10 Measured=",
+ "Bandwidth=10 Measured=-50",
+ )
+
+ for w_line in test_values:
+ content = get_router_status_entry({'w': w_line})
+ self._expect_invalid_rse_attr(content)
+
+ def test_rse_exit_policy(self):
+ """
+ Handles a variety of 'p' lines.
+ """
+
+ test_values = {
+ "reject 1-65535": MicrodescriptorExitPolicy("reject 1-65535"),
+ "accept 80,110,143,443": MicrodescriptorExitPolicy("accept 80,110,143,443"),
+ }
+
+ for p_line, expected in test_values.items():
+ content = get_router_status_entry({'p': p_line})
+ entry = RouterStatusEntry(content, None)
+ self.assertEquals(expected, entry.exit_policy)
+
+ # tries some invalid inputs
+ test_values = (
+ "",
+ "blarg",
+ "reject -50",
+ "accept 80,",
+ )
+
+ for p_line in test_values:
+ content = get_router_status_entry({'p': p_line})
+ self._expect_invalid_rse_attr(content, "exit_policy")
+
+ def test_rse_microdescriptor_hashes(self):
+ """
+ Handles a variety of 'm' lines.
+ """
+
+ test_values = {
+ "8,9,10,11,12":
+ [([8, 9, 10, 11, 12], {})],
+ "8,9,10,11,12 sha256=g1vx9si329muxV3tquWIXXySNOIwRGMeAESKs/v4DWs":
+ [([8, 9, 10, 11, 12], {"sha256": "g1vx9si329muxV3tquWIXXySNOIwRGMeAESKs/v4DWs"})],
+ "8,9,10,11,12 sha256=g1vx9si329muxV md5=3tquWIXXySNOIwRGMeAESKs/v4DWs":
+ [([8, 9, 10, 11, 12], {"sha256": "g1vx9si329muxV", "md5": "3tquWIXXySNOIwRGMeAESKs/v4DWs"})],
+ }
+
+ # we need a document that's a vote
+ mock_document = lambda x: x # just need anything with a __dict__
+ mock_document.__dict__["vote_status"] = "vote"
+
+ for m_line, expected in test_values.items():
+ content = get_router_status_entry({'m': m_line})
+ entry = RouterStatusEntry(content, mock_document)
+ self.assertEquals(expected, entry.microdescriptor_hashes)
+
+ # try without a document
+ content = get_router_status_entry({'m': "8,9,10,11,12"})
+ self._expect_invalid_rse_attr(content, "microdescriptor_hashes")
+
+ # tries some invalid inputs
+ test_values = (
+ "",
+ "4,a,2",
+ "1,2,3 stuff",
+ )
+
+ for m_line in test_values:
+ content = get_router_status_entry({'m': m_line})
+ self.assertRaises(ValueError, RouterStatusEntry, content, mock_document)
+
+ def _expect_invalid_rse_attr(self, content, attr = None, expected_value = None):
+ """
+ Asserts that construction will fail due to content having a malformed
+ attribute. If an attr is provided then we check that it matches an expected
+ value when we're constructed without validation.
+ """
+
+ self.assertRaises(ValueError, RouterStatusEntry, content, None)
+ entry = RouterStatusEntry(content, None, False)
+
+ if attr:
+ self.assertEquals(expected_value, getattr(entry, attr))
+ else:
+ self.assertEquals("caerSidi", entry.nickname)
More information about the tor-commits
mailing list