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 (
20
UnsupportedDisplayType,
25
def threaded_messages(messages):
29
for message in messages:
30
if message.get('in_reply_to') is None:
31
threads[message['message_id']] = [message]
34
pending.append(message)
35
for message in pending:
36
threads[message['in_reply_to']].append(message)
37
return threads.values()
41
"""A memory-backed message store."""
43
def __init__(self, messages):
45
self.messages = messages
47
def get_messages(self, archive_id, query_string):
48
"""Return matching messages.
50
:param archive_id: The archive to retrieve from.
51
:param query_string: Contains 'parameters', which is a JSON-format
52
string describing parameters.
54
query = parse_qs(query_string)
55
parameters = simplejson.loads(query['parameters'][0])
56
order = parameters.get('order')
57
messages = self.messages[archive_id]
59
if order not in SUPPORTED_ORDERS:
60
raise UnsupportedOrder
61
elif order.startswith('thread_'):
62
threaded = threaded_messages(messages)
64
if order == 'thread_subject':
65
threaded.sort(key=lambda t: t[0]['subject'])
66
if order == 'thread_oldest':
67
threaded.sort(key=lambda t: min(m['date'] for m in t))
68
if order == 'thread_newest':
69
threaded.sort(key=lambda t: max(m['date'] for m in t))
70
for thread in threaded:
71
messages.extend(thread)
73
messages.sort(key=lambda m: m[order])
74
display_type = parameters.get('display_type', 'all')
75
if display_type not in SUPPORTED_DISPLAY_TYPES:
76
raise UnsupportedDisplayType
78
for message in messages:
79
if (not parameters['include_hidden']
80
and message.get('hidden', False)):
83
if ('message_ids' in parameters
84
and message['message_id'] not in parameters['message_ids']):
86
message = dict(message)
87
if 'headers' in parameters:
89
(k, v) for k, v in message['headers'].iteritems()
90
if k in parameters['headers'])
91
message['headers'] = headers
92
max_body = parameters.get('max_body_length')
93
if display_type == 'headers-only':
95
elif max_body is not None:
96
message['body'] = message['body'][:max_body]
97
new_messages.append(message)
98
messages = new_messages
99
limit = parameters.get('limit', 100)
100
memo = parameters.get('memo')
101
message_id_indices = dict(
102
(m['message_id'], idx) for idx, m in enumerate(messages))
106
start = message_id_indices[memo.encode('rot13')]
108
previous_memo = messages[start - 1]['message_id'].encode('rot13')
111
end = min(start + limit, len(messages))
112
if end < len(messages):
113
next_memo = messages[end]['message_id'].encode('rot13')
116
messages = messages[start:end]
119
'messages': messages,
120
'next_memo': next_memo,
121
'previous_memo': previous_memo
126
class ForkedFakeService:
127
"""A Grackle service fake, as a ContextManager."""
129
def __init__(self, port, messages=None, write_logs=False):
132
:param port: The tcp port to use.
133
:param messages: A dict of lists of dicts representing messages. The
134
outer dict represents the archive, the list represents the list of
135
messages for that archive.
136
:param write_logs: If true, log messages will be written to stdout.
143
self.messages = messages
144
self.read_end, self.write_end = os.pipe()
145
self.write_logs = write_logs
148
def from_client(client, messages=None):
149
"""Instantiate a ForkedFakeService from the client.
151
:param port: The client to provide service for.
152
:param messages: A dict of lists of dicts representing messages. The
153
outer dict represents the archive, the list represents the list of
154
messages for that archive.
156
return ForkedFakeService(client.port, messages)
159
"""Tell the parent process that the server is ready for writes."""
160
os.write(self.write_end, 'asdf')
165
Fork and start a server in the child. Return when the server is ready
171
os.read(self.read_end, 1)
174
def start_server(self):
175
"""Start the HTTP server."""
176
service = HTTPServer(('', self.port), FakeGrackleRequestHandler)
177
service.store = GrackleStore(self.messages)
178
for archive_id, messages in service.store.messages.iteritems():
179
for message in messages:
180
message.setdefault('headers', {})
184
stream=sys.stderr, level=logging.INFO)
185
service.serve_forever()
187
def __exit__(self, exc_type, exc_val, traceback):
188
os.kill(self.pid, SIGKILL)
191
SUPPORTED_DISPLAY_TYPES = set(['all', 'text-only', 'headers-only'])
194
SUPPORTED_ORDERS = set(
195
['date', 'author', 'subject', 'thread_newest', 'thread_oldest',
199
class FakeGrackleRequestHandler(BaseHTTPRequestHandler):
200
"""A request handler that forwards to server.store."""
202
def __init__(self, *args, **kwargs):
203
"""Constructor. Sets up logging."""
204
self.logger = logging.getLogger('http')
205
BaseHTTPRequestHandler.__init__(self, *args, **kwargs)
208
"""Create a message on POST."""
209
message = self.rfile.read(int(self.headers['content-length']))
210
if message == 'This is a message':
211
self.send_response(httplib.CREATED)
215
self.send_error(httplib.BAD_REQUEST)
218
"""Retrieve a list of messages on GET."""
219
scheme, netloc, path, params, query_string, fragments = (
221
parts = path.split('/')
222
if parts[1] == 'archive':
224
response = self.server.store.get_messages(
225
parts[2], query_string)
226
self.send_response(httplib.OK)
228
self.wfile.write(simplejson.dumps(response))
229
except UnsupportedOrder:
231
httplib.BAD_REQUEST, UnsupportedOrder.__doc__)
233
except UnsupportedDisplayType:
235
httplib.BAD_REQUEST, UnsupportedDisplayType.__doc__)
238
def log_message(self, format, *args):
239
"""Override log_message to use standard Python logging."""
240
message = "%s - - [%s] %s\n" % (
241
self.address_string(), self.log_date_time_string(), format % args)
242
self.logger.info(message)
245
class TestPutMessage(TestCase):
247
def test_put_message(self):
248
client = GrackleClient('localhost', 8436)
249
with ForkedFakeService.from_client(client):
250
client.put_message('arch1', 'asdf', StringIO('This is a message'))
251
with ExpectedException(Exception, 'wtf'):
252
client.put_message('arch1', 'asdf',
253
StringIO('This is not a message'))
256
class TestGetMessages(TestCase):
258
def assertIDOrder(self, ids, messages):
259
self.assertEqual(ids, [m['message_id'] for m in messages])
261
def assertMessageIDs(self, ids, messages):
263
sorted(ids), sorted(messages, key=lambda m: m['message_id']))
265
def test_get_messages(self):
266
client = GrackleClient('localhost', 8435)
267
with ForkedFakeService.from_client(client,
269
[{'message_id': 'foo'},
270
{'message_id': 'bar'}]}):
271
response = client.get_messages('baz')
272
self.assertEqual(['bar', 'foo'], sorted(m['message_id'] for m in
273
response['messages']))
274
self.assertIs(None, response['next_memo'])
275
self.assertIs(None, response['previous_memo'])
277
def test_get_messages_by_id(self):
278
client = GrackleClient('localhost', 8437)
279
with ForkedFakeService.from_client(client,
281
[{'message_id': 'foo'},
282
{'message_id': 'bar'}]}):
283
response = client.get_messages('baz', message_ids=['foo'])
284
message, = response['messages']
285
self.assertEqual('foo', message['message_id'])
287
def test_get_messages_batching(self):
288
client = GrackleClient('localhost', 8438)
289
with ForkedFakeService.from_client(client,
291
[{'message_id': 'foo'},
292
{'message_id': 'bar'}]}):
293
response = client.get_messages('baz', limit=1)
294
self.assertEqual(1, len(response['messages']))
295
messages = response['messages']
296
response = client.get_messages(
297
'baz', limit=1, memo=response['next_memo'])
298
self.assertEqual(1, len(response['messages']))
299
messages.extend(response['messages'])
300
self.assertMessageIDs(['foo', 'bar'], messages)
302
def get_messages_member_order_test(self, key):
303
client = GrackleClient('localhost', 8439)
304
with ForkedFakeService.from_client(client,
305
{'baz': [{'message_id': 'foo', key: '2011-03-25'},
306
{'message_id': 'bar', key: '2011-03-24'}]}):
307
response = client.get_messages('baz')
308
self.assertIDOrder(['foo', 'bar'], response['messages'])
309
response = client.get_messages('baz', order=key)
310
self.assertIDOrder(['bar', 'foo'], response['messages'])
312
def test_get_messages_date_order(self):
313
self.get_messages_member_order_test('date')
315
def test_get_messages_author_order(self):
316
self.get_messages_member_order_test('author')
318
def test_get_messages_subject_order(self):
319
self.get_messages_member_order_test('subject')
321
def test_get_messages_thread_subject_order(self):
322
client = GrackleClient('localhost', 8439)
323
with ForkedFakeService.from_client(client, {'baz': [
324
{'message_id': 'bar', 'subject': 'y'},
325
{'message_id': 'qux', 'subject': 'z'},
326
{'message_id': 'foo', 'subject': 'x', '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='subject')
331
self.assertIDOrder(['foo', 'bar', 'qux'], response['messages'])
332
response = client.get_messages('baz', order='thread_subject')
333
self.assertIDOrder(['bar', 'qux', 'foo'], response['messages'])
335
def test_get_messages_thread_oldest_order(self):
336
client = GrackleClient('localhost', 8439)
337
with ForkedFakeService.from_client(client, {'baz': [
338
{'message_id': 'bar', 'date': 'x'},
339
{'message_id': 'qux', 'date': 'z'},
340
{'message_id': 'foo', 'date': 'y', 'in_reply_to': 'qux'},
342
response = client.get_messages('baz')
343
self.assertIDOrder(['bar', 'qux', 'foo'], response['messages'])
344
response = client.get_messages('baz', order='date')
345
self.assertIDOrder(['bar', 'foo', 'qux'], response['messages'])
346
response = client.get_messages('baz', order='thread_oldest')
347
self.assertIDOrder(['bar', 'qux', 'foo'], response['messages'])
349
def test_get_messages_thread_newest_order(self):
350
client = GrackleClient('localhost', 8439)
351
with ForkedFakeService.from_client(client, {'baz': [
352
{'message_id': 'bar', 'date': 'x'},
353
{'message_id': 'qux', 'date': 'w'},
354
{'message_id': 'foo', 'date': 'y', 'in_reply_to': 'bar'},
355
{'message_id': 'baz', 'date': 'z', 'in_reply_to': 'qux'},
357
response = client.get_messages('baz', order='date')
359
['qux', 'bar', 'foo', 'baz'], response['messages'])
360
response = client.get_messages('baz', order='thread_newest')
362
['bar', 'foo', 'qux', 'baz'], response['messages'])
364
def test_get_messages_unsupported_order(self):
365
client = GrackleClient('localhost', 8439)
366
with ForkedFakeService.from_client(client,
367
{'baz': [{'message_id': 'foo', 'date': '2011-03-25'},
368
{'message_id': 'bar', 'date': '2011-03-24'}]}):
369
with ExpectedException(UnsupportedOrder, ''):
370
client.get_messages('baz', order='nonsense')
372
def test_get_messages_headers_no_headers(self):
373
client = GrackleClient('localhost', 8440)
374
with ForkedFakeService.from_client(client,
376
{'message_id': 'foo'}
378
response = client.get_messages('baz', headers=[
379
'Subject', 'Date', 'X-Launchpad-Message-Rationale'])
380
first_message = response['messages'][0]
381
self.assertEqual('foo', first_message['message_id'])
382
self.assertEqual({}, first_message['headers'])
384
def test_get_messages_headers_exclude_headers(self):
385
client = GrackleClient('localhost', 8441)
386
with ForkedFakeService.from_client(client,
388
{'message_id': 'foo', 'headers': {'From': 'me'}}
390
response = client.get_messages('baz', headers=[
391
'Subject', 'Date', 'X-Launchpad-Message-Rationale'])
392
first_message = response['messages'][0]
393
self.assertEqual('foo', first_message['message_id'])
394
self.assertEqual({}, first_message['headers'])
396
def test_get_messages_headers_include_headers(self):
397
client = GrackleClient('localhost', 8442)
398
with ForkedFakeService.from_client(client,
400
{'message_id': 'foo', 'headers': {'From': 'me', 'To': 'you'}}
402
response = client.get_messages('baz', headers=[
404
first_message = response['messages'][0]
405
self.assertEqual('foo', first_message['message_id'])
406
self.assertEqual({'From': 'me', 'To': 'you'}, first_message['headers'])
408
def test_get_messages_max_body_length(self):
409
client = GrackleClient('localhost', 8443)
410
with ForkedFakeService.from_client(client,
412
{'message_id': 'foo', 'body': u'abcdefghi'}
414
response = client.get_messages('baz', max_body_length=3)
415
first_message = response['messages'][0]
416
self.assertEqual('abc', first_message['body'])
418
def test_include_hidden(self):
419
client = GrackleClient('localhost', 8444)
420
with ForkedFakeService.from_client(client,
422
{'message_id': 'foo', 'hidden': True},
423
{'message_id': 'bar', 'hidden': False}
425
response = client.get_messages('baz', include_hidden=True)
426
self.assertMessageIDs(['bar', 'foo'], response['messages'])
427
response = client.get_messages('baz', include_hidden=False)
428
self.assertMessageIDs(['bar'], response['messages'])
430
def test_display_type_unknown_value(self):
431
client = GrackleClient('localhost', 8445)
432
with ForkedFakeService.from_client(client,
434
{'message_id': 'foo', 'body': u'abcdefghi'}
436
with ExpectedException(UnsupportedDisplayType, ''):
437
client.get_messages('baz', display_type='unknown')
439
def test_display_type_headers_only(self):
440
client = GrackleClient('localhost', 8445)
441
with ForkedFakeService.from_client(client,
443
{'message_id': 'foo',
444
'headers': {'From': 'me', 'To': 'you'},
447
response = client.get_messages('baz', display_type='headers-only')
448
first_message = response['messages'][0]
449
self.assertEqual('foo', first_message['message_id'])
450
self.assertEqual({'From': 'me', 'To': 'you'}, first_message['headers'])
451
self.assertNotIn('body', first_message)