mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-08-26 12:46:08 +00:00
Tearing up a bunch of stuff again. Trying to create decent tests.
Most of the santiago.py::Santiago stuff is irrelevant again. Going test-first is showing me a less terrible interface.
This commit is contained in:
parent
650e7422fe
commit
7c5a8fcee0
@ -80,8 +80,18 @@ class SantiagoHttpListener(santiago.SantiagoListener):
|
|||||||
class SantiagoHttpSender(santiago.SantiagoSender):
|
class SantiagoHttpSender(santiago.SantiagoSender):
|
||||||
"""Responsible for answering HTTP requests."""
|
"""Responsible for answering HTTP requests."""
|
||||||
|
|
||||||
|
import urllib, urllib2
|
||||||
|
|
||||||
@cherrypy.tools.json_out()
|
@cherrypy.tools.json_out()
|
||||||
def proxy(self, key, service, server, hops=3):
|
def proxy(self, key, service, server, hops=3):
|
||||||
"""Forwards on a request to another Santiago."""
|
"""Forwards on a request to another Santiago."""
|
||||||
|
|
||||||
return super(SantiagoHttpSender, self).proxy(key, service, server, hops)
|
return super(SantiagoHttpSender, self).proxy(key, service, server, hops)
|
||||||
|
|
||||||
|
@cherrypy.tools.json_out()
|
||||||
|
def send(self):
|
||||||
|
"""Send messages to other Santiagi."""
|
||||||
|
|
||||||
|
for message in super(SantiagoSender, self).send():
|
||||||
|
if message["location"].startswith("http"):
|
||||||
|
urllib2.Request(message["location"],urllib.urlencode(message))
|
||||||
|
|||||||
@ -67,10 +67,35 @@ class Santiago(object):
|
|||||||
Santiago services. A Santiago has no idea of and is not responsible for
|
Santiago services. A Santiago has no idea of and is not responsible for
|
||||||
anybody else's services.
|
anybody else's services.
|
||||||
|
|
||||||
|
Serving-related variables:
|
||||||
|
|
||||||
|
:hosting: service to location mappings: This dictionary maps service
|
||||||
|
names to service locations.
|
||||||
|
|
||||||
|
:keys: key to service mappings: This dictionary maps keys to service
|
||||||
|
names.
|
||||||
|
|
||||||
|
Between the two, we can provide services for keys at particular
|
||||||
|
locations. These aren't necessarily services owned by this box, it
|
||||||
|
merely points the way and acts as a directory service.
|
||||||
|
|
||||||
|
Client-related variables:
|
||||||
|
|
||||||
|
:servers: This dual-key dictionary stores service: key: location
|
||||||
|
mappings, allowing for fast service-based lookup when I'm seeking
|
||||||
|
somebody to perform a specific service for me.
|
||||||
|
|
||||||
|
Both the client and server dictionaries can contain one another's data.
|
||||||
|
I'm not sure whether they should or not, yet. The data separation seems
|
||||||
|
valuable but perhaps highly over-engineered.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
self.instance = instance
|
self.instance = instance
|
||||||
|
# what I host where
|
||||||
self.hosting = self.load_dict("hosting")
|
self.hosting = self.load_dict("hosting")
|
||||||
|
# who I host for
|
||||||
self.keys = self.load_dict("keys")
|
self.keys = self.load_dict("keys")
|
||||||
|
# other folks
|
||||||
self.servers = self.load_dict("servers")
|
self.servers = self.load_dict("servers")
|
||||||
|
|
||||||
self.listeners = list()
|
self.listeners = list()
|
||||||
@ -101,30 +126,22 @@ class Santiago(object):
|
|||||||
# Server-related tags
|
# Server-related tags
|
||||||
# -------------------
|
# -------------------
|
||||||
|
|
||||||
def provide_at_location(self, service, location):
|
def provide_service(self, key, service, location):
|
||||||
"""Serve service at location.
|
"""Serve a service for user at location.
|
||||||
|
|
||||||
post::
|
post::
|
||||||
|
|
||||||
location in self.hosting[service]
|
location in self.hosting[service]
|
||||||
|
|
||||||
"""
|
|
||||||
self.hosting[service].append(location)
|
|
||||||
|
|
||||||
def provide_for_key(self, service, key):
|
|
||||||
"""Serve service for user.
|
|
||||||
|
|
||||||
post::
|
|
||||||
|
|
||||||
service in self.keys[key]
|
service in self.keys[key]
|
||||||
|
|
||||||
"""
|
"""
|
||||||
self.keys[key].append(service)
|
self.keys[key].append(service)
|
||||||
|
self.hosting[service].append(location)
|
||||||
|
|
||||||
# client-related tags
|
# client-related tags
|
||||||
# -------------------
|
# -------------------
|
||||||
|
|
||||||
def learn_service(self, service, key, locations):
|
def learn_service(self, key, service, locations):
|
||||||
"""Learn a service to use, as a client.
|
"""Learn a service to use, as a client.
|
||||||
|
|
||||||
post::
|
post::
|
||||||
@ -132,7 +149,7 @@ class Santiago(object):
|
|||||||
forall(locations, lambda x: x in self.servers[service][key])
|
forall(locations, lambda x: x in self.servers[service][key])
|
||||||
|
|
||||||
"""
|
"""
|
||||||
self.servers[service][key] += locations
|
self.servers[service][key].extend(locations)
|
||||||
|
|
||||||
def consume_service(self, service, key):
|
def consume_service(self, service, key):
|
||||||
return self.servers[service][key]
|
return self.servers[service][key]
|
||||||
@ -154,7 +171,7 @@ class Santiago(object):
|
|||||||
"""Provide a requested service to a client."""
|
"""Provide a requested service to a client."""
|
||||||
|
|
||||||
if santiagi is not None:
|
if santiagi is not None:
|
||||||
self.learn_service("santiago", key, santiagi)
|
self.learn_service(key, "santiago", santiagi)
|
||||||
|
|
||||||
if not self.am_i(server):
|
if not self.am_i(server):
|
||||||
self.proxy(key, service, server, hops=hops)
|
self.proxy(key, service, server, hops=hops)
|
||||||
@ -286,9 +303,6 @@ if __name__ == "__main__":
|
|||||||
santiago.add_sender(http_sender)
|
santiago.add_sender(http_sender)
|
||||||
|
|
||||||
# TODO move this into the table loading.
|
# TODO move this into the table loading.
|
||||||
santiago.provide_at_location("wiki", "192.168.0.13")
|
santiago.provide_service("james", "wiki", "192.168.0.13")
|
||||||
santiago.provide_for_key("wiki", "james")
|
|
||||||
santiago.max_hops = 3
|
|
||||||
santiago.proxy_list = ("tor")
|
|
||||||
|
|
||||||
cherrypy.quickstart(http_listener)
|
cherrypy.quickstart(http_listener)
|
||||||
|
|||||||
@ -23,7 +23,25 @@ class SantiagoTest(unittest.TestCase):
|
|||||||
"""The base class for tests."""
|
"""The base class for tests."""
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.santiago = santiago.Santiago("0x1")
|
super(TestServing, self).setUp()
|
||||||
|
|
||||||
|
port_a = "localhost:9000"
|
||||||
|
port_b = "localhost:8000"
|
||||||
|
|
||||||
|
listeners_a = [santiago.SantiagoListener(port_a)]
|
||||||
|
senders_a = [santiago.SantiagoSender()]
|
||||||
|
listeners_b = [santiago.SantiagoListener(port_b)]
|
||||||
|
senders_b = [santiago.SantiagoSender()]
|
||||||
|
|
||||||
|
hosting_a = { "b": { "santiago": [ port_a ]}}
|
||||||
|
consuming_a = { "santiagao": { "b": [ port_b ]}}
|
||||||
|
|
||||||
|
hosting_b = { "a": { "santiago": [ port_b ],
|
||||||
|
"wiki": [ "localhost:8001" ]}}
|
||||||
|
consuming_b = { "santiagao": { "a": [ port_a ]}}
|
||||||
|
|
||||||
|
self.santiago_a = Santiago(listeners_a, senders_a, hosting_a, consuming_a)
|
||||||
|
self.santiago_b = Santiago(listeners_b, senders_b, hosting_b, consuming_b)
|
||||||
|
|
||||||
if sys.version_info < (2, 7):
|
if sys.version_info < (2, 7):
|
||||||
"""Add a poor man's forward compatibility."""
|
"""Add a poor man's forward compatibility."""
|
||||||
@ -35,95 +53,147 @@ class SantiagoTest(unittest.TestCase):
|
|||||||
if not a in b:
|
if not a in b:
|
||||||
raise self.ContainsError("%s not in %s" % (a, b))
|
raise self.ContainsError("%s not in %s" % (a, b))
|
||||||
|
|
||||||
class TestDataModel(SantiagoTest):
|
class TestServerInitialRequest(SantiagoTest):
|
||||||
"""Test adding and removing services and data."""
|
"""Test how the Santiago server replies to initial service requests.
|
||||||
|
|
||||||
def test_add_listener(self):
|
TODO: tests for:
|
||||||
"""Verify that we can add a listener."""
|
|
||||||
|
|
||||||
http_listener = SantiagoHttpListener(self.santiago, "localhost:8080")
|
|
||||||
self.santiago.add_listener(http_listener)
|
|
||||||
|
|
||||||
self.assertIn(http_listener, self.santiago.listeners)
|
|
||||||
|
|
||||||
def test_add_sender(self):
|
|
||||||
"""Verify that we can add a sender."""
|
|
||||||
|
|
||||||
http_sender = SantiagoHttpSender(self.santiago)
|
|
||||||
self.santiago.add_sender(http_sender)
|
|
||||||
|
|
||||||
self.assertIn(http_sender, self.santiago.senders)
|
|
||||||
|
|
||||||
def test_provide_at_location(self):
|
|
||||||
"""Verify that we can provide a service somewhere."""
|
|
||||||
|
|
||||||
service, location = ("something", "there")
|
|
||||||
self.santiago.provide_at_location(service, location)
|
|
||||||
|
|
||||||
self.assertIn(location, self.santiago.hosting[service])
|
|
||||||
|
|
||||||
def test_provide_for_key(self):
|
|
||||||
"""Verify we can provide a specific service to someone."""
|
|
||||||
|
|
||||||
service, key = ("something", "0x1")
|
|
||||||
self.santiago.provide_for_key(service, key)
|
|
||||||
|
|
||||||
self.assertIn(service, self.santiago.keys[key])
|
|
||||||
|
|
||||||
|
|
||||||
class TestServing(SantiagoTest):
|
|
||||||
"""TODO: tests for:
|
|
||||||
|
|
||||||
(normal serving + proxying) * (learning santiagi + not learning)
|
(normal serving + proxying) * (learning santiagi + not learning)
|
||||||
|
|
||||||
"""
|
"""
|
||||||
def setUp(self):
|
def test_acknowledgement(self):
|
||||||
super(TestServing, self).setUp()
|
"""If B receives an authorized request, then it replies with a location.
|
||||||
|
|
||||||
self.santiago.provide_at_location("wiki", "192.168.0.13")
|
An "authorized request" in this case is for a service from a client that
|
||||||
self.santiago.provide_for_key("wiki", "0x2")
|
B is willing to host that service for.
|
||||||
|
|
||||||
self.listener = santiago.SantiagoListener(self.santiago, "localhost:8080")
|
In this case, B will answer with the wiki's location.
|
||||||
self.sender = santiago.SantiagoSender(self.santiago)
|
|
||||||
self.santiago.add_listener(self.listener)
|
|
||||||
self.santiago.add_sender(self.sender)
|
|
||||||
|
|
||||||
def test_successful_serve(self):
|
"""
|
||||||
"""Make sure we get an expected, successful serving message back."""
|
self.santiago_b.receive(from_=None, to=None,
|
||||||
|
client="a", host="b",
|
||||||
|
service="wiki", reply_to=None)
|
||||||
|
|
||||||
self.listener.serve("0x2", "wiki", "0x1", 0, None)
|
self.assertEqual(self.santiago_b.outgoing_messages,
|
||||||
expected = [
|
[{"from": "b",
|
||||||
{ "to": "0x2",
|
"to": "a",
|
||||||
"location": ["192.168.0.13"],
|
"client": "a",
|
||||||
"reply-to": self.listener.location, }, ]
|
"host": "b",
|
||||||
|
"service": "wiki",
|
||||||
|
"locations": ["192.168.0.13"],
|
||||||
|
"reply-to": "localhost:8000"}])
|
||||||
|
|
||||||
self.assertEqual(self.sender.send(), expected)
|
def test_negative_acknowledgement_bad_service(self)
|
||||||
|
"""Does B reject requests for unsupported services?
|
||||||
|
|
||||||
|
In this case, B should reply with an empty list of locations.
|
||||||
|
|
||||||
|
"""
|
||||||
|
self.santiago_b.receive(from_=None, to=None,
|
||||||
|
client="a", host="b",
|
||||||
|
service="wiki", reply_to=None)
|
||||||
|
|
||||||
|
self.assertEqual(self.santiago_b.outgoing_messages,
|
||||||
|
[{"from": "b",
|
||||||
|
"to": "a",
|
||||||
|
"client": "a",
|
||||||
|
"host": "b",
|
||||||
|
"service": "wiki",
|
||||||
|
"locations": [],
|
||||||
|
"reply-to": "localhost:8000"}])
|
||||||
|
|
||||||
|
def test_deny_bad_key(self):
|
||||||
|
"""If B receives a request from an unauthorized key, it does not reply.
|
||||||
|
|
||||||
|
An "unauthorized request" in this case is for a service from a client
|
||||||
|
that B does not trust. This is different than clients B hosts no
|
||||||
|
services for.
|
||||||
|
|
||||||
|
In this case, B will never answer the request.
|
||||||
|
|
||||||
|
"""
|
||||||
|
self.santiago_b.receive(from_=None, to=None,
|
||||||
|
client="z", host="b",
|
||||||
|
service="wiki", reply_to=None)
|
||||||
|
|
||||||
|
self.assertEqual(self.santiago_b.outgoing_messages, [])
|
||||||
|
|
||||||
|
def test_learn_santaigo(self):
|
||||||
|
"""Does B learn new Santiago locations from trusted requests?
|
||||||
|
|
||||||
|
If A sends B a request with a new Santiago location, B should learn it.
|
||||||
|
|
||||||
|
"""
|
||||||
|
self.santiago_b.receive(from_=None, to=None,
|
||||||
|
client="a", host="b",
|
||||||
|
service="wiki", reply_to="localhost:9001")
|
||||||
|
|
||||||
|
self.assertEqual(self.santiago_b.consuming["santiago"]["a"],
|
||||||
|
["localhost:9000", "localhost:9001"])
|
||||||
|
|
||||||
def test_handle_requests_once(self):
|
def test_handle_requests_once(self):
|
||||||
"""Verify that we send each request out only once."""
|
"""Verify that we reply to each request only once."""
|
||||||
|
|
||||||
self.listener.serve("0x2", "wiki", "0x1", 0, None)
|
self.santiago_b.receive(from_=None, to=None,
|
||||||
self.sender.send()
|
client="a", host="b",
|
||||||
|
service="wiki", reply_to=None)
|
||||||
|
self.santiago_b.process()
|
||||||
|
|
||||||
self.assertEqual(self.sender.send(), list())
|
self.assertEqual(self.santiago_b.outgoing_messages, [])
|
||||||
|
|
||||||
class TestConsuming(SantiagoTest):
|
class TestClientInitialRequest(SantiagoTest):
|
||||||
"""TODO: tests for:
|
"""Does the client send a correctly formed request?
|
||||||
|
|
||||||
(learning services)
|
In these tests, we're sending requests to a mock listener which merely
|
||||||
|
records that the requests were well-formed.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
def setUp(self):
|
||||||
|
super(SantiagoTest, self).setUp()
|
||||||
|
|
||||||
|
import cherrypy
|
||||||
|
class RequestReceiver(object):
|
||||||
|
|
||||||
|
def index(self, *args, **kwargs):
|
||||||
|
self.args = *args
|
||||||
|
self.kwargs = **kwargs
|
||||||
|
|
||||||
|
index.exposed = True
|
||||||
|
self.socket_port = 8000
|
||||||
|
|
||||||
|
self.receiver = RequestReceiver()
|
||||||
|
cherrypy.quickstart(self.receiver)
|
||||||
|
|
||||||
|
def test_request(self):
|
||||||
|
"""Verify that A queues a properly formatted initial request."""
|
||||||
|
|
||||||
|
self.santiago_a.request(from_="a", to="b",
|
||||||
|
client="a", host="b",
|
||||||
|
service="wiki", reply_to="localhost:9001")
|
||||||
|
|
||||||
|
self.assertEqual(self.santiago_a.outgoing_messages,
|
||||||
|
[{ "from": "a", "to": "b",
|
||||||
|
"client": "a", "host": "b",
|
||||||
|
"service": "wiki", "reply-to": "localhost:9001"})]
|
||||||
|
|
||||||
|
def test_request(self):
|
||||||
|
"""Verify that A sends out a properly formatted initial request."""
|
||||||
|
|
||||||
|
self.santiago_a.request(from_="a", to="b",
|
||||||
|
client="a", host="b",
|
||||||
|
service="wiki", reply_to="localhost:9001")
|
||||||
|
|
||||||
|
self.santiago_a.process()
|
||||||
|
|
||||||
|
self.assertEqual(self.receiver.kwargs,
|
||||||
|
[{ "from": "a", "to": "b",
|
||||||
|
"client": "a", "host": "b",
|
||||||
|
"service": "wiki", "reply-to": "localhost:9001"})]
|
||||||
|
|
||||||
|
class TestServerInitialResponse(SantiagoTest):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
class TestInitialRequest(SantiagoTest):
|
class TestClientInitialResponse(SantiagoTest):
|
||||||
"""Testing the initial request.
|
|
||||||
|
|
||||||
Does Santiago produce well-formed output when creating a service request?
|
|
||||||
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
class TestInitialResponse(SantiagoTest):
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
class TestForwardedRequest(SantiagoTest):
|
class TestForwardedRequest(SantiagoTest):
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user