1
from BaseHTTPServer import (
3
BaseHTTPRequestHandler,
8
from signal import SIGKILL
10
from StringIO import StringIO
12
from unittest import TestCase
13
from urlparse import urlparse
14
from urlparse import parse_qs
16
from testtools import ExpectedException
18
from grackle.client import (
24
def threaded_messages(messages):
28
for message in messages:
29
if message.get('in_reply_to') is None:
30
threads[message['message_id']] = [message]
33
pending.append(message)
34
for message in pending:
35
threads[message['in_reply_to']].append(message)
36
return threads.values()
40
"""A memory-backed message store."""
42
def __init__(self, messages):
44
self.messages = messages
46
def get_messages(self, archive_id, query_string):
47
"""Return matching messages.
49
:param archive_id: The archive to retrieve from.
50
:param query_string: Contains 'parameters', which is a JSON-format
51
string describing parameters.
53
query = parse_qs(query_string)
54
parameters = simplejson.loads(query['parameters'][0])
55
order = parameters.get('order')
56
messages = self.messages[archive_id]
58
if order not in SUPPORTED_ORDERS:
59
raise UnsupportedOrder
60
elif order.startswith('thread_'):
61
threaded = threaded_messages(messages)
63
if order == 'thread_subject':
64
threaded.sort(key=lambda t: t[0]['subject'])
65
if order == 'thread_oldest':
66
threaded.sort(key=lambda t: min(m['date'] for m in t))
67
if order == 'thread_newest':
68
threaded.sort(key=lambda t: max(m['date'] for m in t))
69
for thread in threaded:
70
messages.extend(thread)
72
messages.sort(key=lambda m: m[order])
74
for message in messages:
75
if (not parameters['include_hidden']
76
and message.get('hidden', False)):
79
if ('message_ids' in parameters
80
and message['message_id'] not in parameters['message_ids']):
82
message = dict(message)
83
if 'headers' in parameters:
85
(k, v) for k, v in message['headers'].iteritems()
86
if k in parameters['headers'])
87
message['headers'] = headers
88
max_body = parameters.get('max_body_length')
89
if max_body is not None:
90
message['body'] = message['body'][:max_body]
91
new_messages.append(message)
92
messages = new_messages
93
limit = parameters.get('limit', 100)
94
memo = parameters.get('memo')
95
message_id_indices = dict(
96
(m['message_id'], idx) for idx, m in enumerate(messages))
100
start = message_id_indices[memo.encode('rot13')]
102
previous_memo = messages[start - 1]['message_id'].encode('rot13')
105
end = min(start + limit, len(messages))
106
if end < len(messages):
107
next_memo = messages[end]['message_id'].encode('rot13')
110
messages = messages[start:end]
113
'messages': messages,
114
'next_memo': next_memo,
115
'previous_memo': previous_memo
121
"""A Grackle service fake, as a ContextManager."""
123
def __init__(self, port, messages=None, write_logs=False):
125
:param port: The tcp port to use
126
:param messages: A dict of lists of dicts representing messages. The
127
outer dict represents the archive, the list represents the list of
128
messages for that archive.
129
:param write_logs: If true, log messages will be written to stdout.
136
self.messages = messages
137
self.read_end, self.write_end = os.pipe()
138
self.write_logs = write_logs
141
def from_client(client, messages=None):
142
"""Instantiate a ForkedFake from the client.
144
:param port: The client to provide service for.
145
:param messages: A dict of lists of dicts representing messages. The
146
outer dict represents the archive, the list represents the list of
147
messages for that archive.
149
return ForkedFake(client.port, messages)
152
"""Tell the parent process that the server is ready for writes."""
153
os.write(self.write_end, 'asdf')
158
Fork and start a server in the child. Return when the server is ready
164
os.read(self.read_end, 1)
167
def start_server(self):
168
"""Start the HTTP server."""
169
service = HTTPServer(('', self.port), FakeGrackleRequestHandler)
170
service.store = GrackleStore(self.messages)
171
for archive_id, messages in service.store.messages.iteritems():
172
for message in messages:
173
message.setdefault('headers', {})
177
stream=sys.stderr, level=logging.INFO)
178
service.serve_forever()
180
def __exit__(self, exc_type, exc_val, traceback):
181
os.kill(self.pid, SIGKILL)
184
SUPPORTED_ORDERS = set(
185
['date', 'author', 'subject', 'thread_newest', 'thread_oldest',
189
class FakeGrackleRequestHandler(BaseHTTPRequestHandler):
190
"""A request handler that forwards to server.store."""
192
def __init__(self, *args, **kwargs):
193
"""Constructor. Sets up logging."""
194
self.logger = logging.getLogger('http')
195
BaseHTTPRequestHandler.__init__(self, *args, **kwargs)
198
"""Create a message on POST."""
199
message = self.rfile.read(int(self.headers['content-length']))
200
if message == 'This is a message':
201
self.send_response(httplib.CREATED)
205
self.send_error(httplib.BAD_REQUEST)
208
"""Retrieve a list of messages on GET."""
209
scheme, netloc, path, params, query_string, fragments = (
211
parts = path.split('/')
212
if parts[1] == 'archive':
214
response = self.server.store.get_messages(
215
parts[2], query_string)
216
self.send_response(httplib.OK)
218
self.wfile.write(simplejson.dumps(response))
219
except UnsupportedOrder:
220
self.send_response(httplib.BAD_REQUEST)
221
self.wfile.write('Unsupported order')
224
def log_message(self, format, *args):
225
"""Override log_message to use standard Python logging."""
226
message = "%s - - [%s] %s\n" % (
227
self.address_string(), self.log_date_time_string(), format % args)
228
self.logger.info(message)
231
class TestPutMessage(TestCase):
233
def test_put_message(self):
234
client = GrackleClient('localhost', 8436)
235
with ForkedFake.from_client(client):
236
client.put_message('arch1', 'asdf', StringIO('This is a message'))
237
with ExpectedException(Exception, 'wtf'):
238
client.put_message('arch1', 'asdf',
239
StringIO('This is not a message'))
242
class TestGetMessages(TestCase):
244
def assertIDOrder(self, ids, messages):
245
self.assertEqual(ids, [m['message_id'] for m in messages])
247
def assertMessageIDs(self, ids, messages):
249
sorted(ids), sorted(messages, key=lambda m: m['message_id']))
251
def test_get_messages(self):
252
client = GrackleClient('localhost', 8435)
253
with ForkedFake.from_client(client,
255
[{'message_id': 'foo'},
256
{'message_id': 'bar'}]}):
257
response = client.get_messages('baz')
258
self.assertEqual(['bar', 'foo'], sorted(m['message_id'] for m in
259
response['messages']))
260
self.assertIs(None, response['next_memo'])
261
self.assertIs(None, response['previous_memo'])
263
def test_get_messages_by_id(self):
264
client = GrackleClient('localhost', 8437)
265
with ForkedFake.from_client(client,
267
[{'message_id': 'foo'},
268
{'message_id': 'bar'}]}):
269
response = client.get_messages('baz', message_ids=['foo'])
270
message, = response['messages']
271
self.assertEqual('foo', message['message_id'])
273
def test_get_messages_batching(self):
274
client = GrackleClient('localhost', 8438)
275
with ForkedFake.from_client(client,
277
[{'message_id': 'foo'},
278
{'message_id': 'bar'}]}):
279
response = client.get_messages('baz', limit=1)
280
self.assertEqual(1, len(response['messages']))
281
messages = response['messages']
282
response = client.get_messages(
283
'baz', limit=1, memo=response['next_memo'])
284
self.assertEqual(1, len(response['messages']))
285
messages.extend(response['messages'])
286
self.assertMessageIDs(['foo', 'bar'], messages)
288
def get_messages_member_order_test(self, key):
289
client = GrackleClient('localhost', 8439)
290
with ForkedFake.from_client(client,
291
{'baz': [{'message_id': 'foo', key: '2011-03-25'},
292
{'message_id': 'bar', key: '2011-03-24'}]}):
293
response = client.get_messages('baz')
294
self.assertIDOrder(['foo', 'bar'], response['messages'])
295
response = client.get_messages('baz', order=key)
296
self.assertIDOrder(['bar', 'foo'], response['messages'])
298
def test_get_messages_date_order(self):
299
self.get_messages_member_order_test('date')
301
def test_get_messages_author_order(self):
302
self.get_messages_member_order_test('author')
304
def test_get_messages_subject_order(self):
305
self.get_messages_member_order_test('subject')
307
def test_get_messages_thread_subject_order(self):
308
client = GrackleClient('localhost', 8439)
309
with ForkedFake.from_client(client, {'baz': [
310
{'message_id': 'bar', 'subject': 'y'},
311
{'message_id': 'qux', 'subject': 'z'},
312
{'message_id': 'foo', 'subject': 'x', 'in_reply_to': 'qux'},
314
response = client.get_messages('baz')
315
self.assertIDOrder(['bar', 'qux', 'foo'], response['messages'])
316
response = client.get_messages('baz', order='subject')
317
self.assertIDOrder(['foo', 'bar', 'qux'], response['messages'])
318
response = client.get_messages('baz', order='thread_subject')
319
self.assertIDOrder(['bar', 'qux', 'foo'], response['messages'])
321
def test_get_messages_thread_oldest_order(self):
322
client = GrackleClient('localhost', 8439)
323
with ForkedFake.from_client(client, {'baz': [
324
{'message_id': 'bar', 'date': 'x'},
325
{'message_id': 'qux', 'date': 'z'},
326
{'message_id': 'foo', 'date': 'y', 'in_reply_to': 'qux'},
328
response = client.get_messages('baz')
329
self.assertIDOrder(['bar', 'qux', 'foo'], response['messages'])
330
response = client.get_messages('baz', order='date')
331
self.assertIDOrder(['bar', 'foo', 'qux'], response['messages'])
332
response = client.get_messages('baz', order='thread_oldest')
333
self.assertIDOrder(['bar', 'qux', 'foo'], response['messages'])
335
def test_get_messages_thread_newest_order(self):
336
client = GrackleClient('localhost', 8439)
337
with ForkedFake.from_client(client, {'baz': [
338
{'message_id': 'bar', 'date': 'x'},
339
{'message_id': 'qux', 'date': 'w'},
340
{'message_id': 'foo', 'date': 'y', 'in_reply_to': 'bar'},
341
{'message_id': 'baz', 'date': 'z', 'in_reply_to': 'qux'},
343
response = client.get_messages('baz', order='date')
345
['qux', 'bar', 'foo', 'baz'], response['messages'])
346
response = client.get_messages('baz', order='thread_newest')
348
['bar', 'foo', 'qux', 'baz'], response['messages'])
350
def test_get_messages_unsupported_order(self):
351
client = GrackleClient('localhost', 8439)
352
with ForkedFake.from_client(client,
353
{'baz': [{'message_id': 'foo', 'date': '2011-03-25'},
354
{'message_id': 'bar', 'date': '2011-03-24'}]}):
355
with ExpectedException(UnsupportedOrder, ''):
356
client.get_messages('baz', order='nonsense')
358
def test_get_messages_headers_no_headers(self):
359
client = GrackleClient('localhost', 8440)
360
with ForkedFake.from_client(client,
362
{'message_id': 'foo'}
364
response = client.get_messages('baz', headers=[
365
'Subject', 'Date', 'X-Launchpad-Message-Rationale'])
366
first_message = response['messages'][0]
367
self.assertEqual('foo', first_message['message_id'])
368
self.assertEqual({}, first_message['headers'])
370
def test_get_messages_headers_exclude_headers(self):
371
client = GrackleClient('localhost', 8441)
372
with ForkedFake.from_client(client,
374
{'message_id': 'foo', 'headers': {'From': 'me'}}
376
response = client.get_messages('baz', headers=[
377
'Subject', 'Date', 'X-Launchpad-Message-Rationale'])
378
first_message = response['messages'][0]
379
self.assertEqual('foo', first_message['message_id'])
380
self.assertEqual({}, first_message['headers'])
382
def test_get_messages_headers_include_headers(self):
383
client = GrackleClient('localhost', 8442)
384
with ForkedFake.from_client(client,
386
{'message_id': 'foo', 'headers': {'From': 'me', 'To': 'you'}}
388
response = client.get_messages('baz', headers=[
390
first_message = response['messages'][0]
391
self.assertEqual('foo', first_message['message_id'])
392
self.assertEqual({'From': 'me', 'To': 'you'}, first_message['headers'])
394
def test_get_messages_max_body_length(self):
395
client = GrackleClient('localhost', 8443)
396
with ForkedFake.from_client(client,
398
{'message_id': 'foo', 'body': u'abcdefghi'}
400
response = client.get_messages('baz', max_body_length=3)
401
first_message = response['messages'][0]
402
self.assertEqual('abc', first_message['body'])
404
def test_include_hidden(self):
405
client = GrackleClient('localhost', 8444)
406
with ForkedFake.from_client(client,
408
{'message_id': 'foo', 'hidden': True},
409
{'message_id': 'bar', 'hidden': False}
411
response = client.get_messages('baz', include_hidden=True)
412
self.assertMessageIDs(['bar', 'foo'], response['messages'])
413
response = client.get_messages('baz', include_hidden=False)
414
self.assertMessageIDs(['bar'], response['messages'])