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:
230
self.send_response(httplib.BAD_REQUEST)
231
self.wfile.write('Unsupported order')
234
def log_message(self, format, *args):
235
"""Override log_message to use standard Python logging."""
236
message = "%s - - [%s] %s\n" % (
237
self.address_string(), self.log_date_time_string(), format % args)
238
self.logger.info(message)
241
class TestPutMessage(TestCase):
243
def test_put_message(self):
244
client = GrackleClient('localhost', 8436)
245
with ForkedFakeService.from_client(client):
246
client.put_message('arch1', 'asdf', StringIO('This is a message'))
247
with ExpectedException(Exception, 'wtf'):
248
client.put_message('arch1', 'asdf',
249
StringIO('This is not a message'))
252
class TestGetMessages(TestCase):
254
def assertIDOrder(self, ids, messages):
255
self.assertEqual(ids, [m['message_id'] for m in messages])
257
def assertMessageIDs(self, ids, messages):
259
sorted(ids), sorted(messages, key=lambda m: m['message_id']))
261
def test_get_messages(self):
262
client = GrackleClient('localhost', 8435)
263
with ForkedFakeService.from_client(client,
265
[{'message_id': 'foo'},
266
{'message_id': 'bar'}]}):
267
response = client.get_messages('baz')
268
self.assertEqual(['bar', 'foo'], sorted(m['message_id'] for m in
269
response['messages']))
270
self.assertIs(None, response['next_memo'])
271
self.assertIs(None, response['previous_memo'])
273
def test_get_messages_by_id(self):
274
client = GrackleClient('localhost', 8437)
275
with ForkedFakeService.from_client(client,
277
[{'message_id': 'foo'},
278
{'message_id': 'bar'}]}):
279
response = client.get_messages('baz', message_ids=['foo'])
280
message, = response['messages']
281
self.assertEqual('foo', message['message_id'])
283
def test_get_messages_batching(self):
284
client = GrackleClient('localhost', 8438)
285
with ForkedFakeService.from_client(client,
287
[{'message_id': 'foo'},
288
{'message_id': 'bar'}]}):
289
response = client.get_messages('baz', limit=1)
290
self.assertEqual(1, len(response['messages']))
291
messages = response['messages']
292
response = client.get_messages(
293
'baz', limit=1, memo=response['next_memo'])
294
self.assertEqual(1, len(response['messages']))
295
messages.extend(response['messages'])
296
self.assertMessageIDs(['foo', 'bar'], messages)
298
def get_messages_member_order_test(self, key):
299
client = GrackleClient('localhost', 8439)
300
with ForkedFakeService.from_client(client,
301
{'baz': [{'message_id': 'foo', key: '2011-03-25'},
302
{'message_id': 'bar', key: '2011-03-24'}]}):
303
response = client.get_messages('baz')
304
self.assertIDOrder(['foo', 'bar'], response['messages'])
305
response = client.get_messages('baz', order=key)
306
self.assertIDOrder(['bar', 'foo'], response['messages'])
308
def test_get_messages_date_order(self):
309
self.get_messages_member_order_test('date')
311
def test_get_messages_author_order(self):
312
self.get_messages_member_order_test('author')
314
def test_get_messages_subject_order(self):
315
self.get_messages_member_order_test('subject')
317
def test_get_messages_thread_subject_order(self):
318
client = GrackleClient('localhost', 8439)
319
with ForkedFakeService.from_client(client, {'baz': [
320
{'message_id': 'bar', 'subject': 'y'},
321
{'message_id': 'qux', 'subject': 'z'},
322
{'message_id': 'foo', 'subject': 'x', 'in_reply_to': 'qux'},
324
response = client.get_messages('baz')
325
self.assertIDOrder(['bar', 'qux', 'foo'], response['messages'])
326
response = client.get_messages('baz', order='subject')
327
self.assertIDOrder(['foo', 'bar', 'qux'], response['messages'])
328
response = client.get_messages('baz', order='thread_subject')
329
self.assertIDOrder(['bar', 'qux', 'foo'], response['messages'])
331
def test_get_messages_thread_oldest_order(self):
332
client = GrackleClient('localhost', 8439)
333
with ForkedFakeService.from_client(client, {'baz': [
334
{'message_id': 'bar', 'date': 'x'},
335
{'message_id': 'qux', 'date': 'z'},
336
{'message_id': 'foo', 'date': 'y', 'in_reply_to': 'qux'},
338
response = client.get_messages('baz')
339
self.assertIDOrder(['bar', 'qux', 'foo'], response['messages'])
340
response = client.get_messages('baz', order='date')
341
self.assertIDOrder(['bar', 'foo', 'qux'], response['messages'])
342
response = client.get_messages('baz', order='thread_oldest')
343
self.assertIDOrder(['bar', 'qux', 'foo'], response['messages'])
345
def test_get_messages_thread_newest_order(self):
346
client = GrackleClient('localhost', 8439)
347
with ForkedFakeService.from_client(client, {'baz': [
348
{'message_id': 'bar', 'date': 'x'},
349
{'message_id': 'qux', 'date': 'w'},
350
{'message_id': 'foo', 'date': 'y', 'in_reply_to': 'bar'},
351
{'message_id': 'baz', 'date': 'z', 'in_reply_to': 'qux'},
353
response = client.get_messages('baz', order='date')
355
['qux', 'bar', 'foo', 'baz'], response['messages'])
356
response = client.get_messages('baz', order='thread_newest')
358
['bar', 'foo', 'qux', 'baz'], response['messages'])
360
def test_get_messages_unsupported_order(self):
361
client = GrackleClient('localhost', 8439)
362
with ForkedFakeService.from_client(client,
363
{'baz': [{'message_id': 'foo', 'date': '2011-03-25'},
364
{'message_id': 'bar', 'date': '2011-03-24'}]}):
365
with ExpectedException(UnsupportedOrder, ''):
366
client.get_messages('baz', order='nonsense')
368
def test_get_messages_headers_no_headers(self):
369
client = GrackleClient('localhost', 8440)
370
with ForkedFakeService.from_client(client,
372
{'message_id': 'foo'}
374
response = client.get_messages('baz', headers=[
375
'Subject', 'Date', 'X-Launchpad-Message-Rationale'])
376
first_message = response['messages'][0]
377
self.assertEqual('foo', first_message['message_id'])
378
self.assertEqual({}, first_message['headers'])
380
def test_get_messages_headers_exclude_headers(self):
381
client = GrackleClient('localhost', 8441)
382
with ForkedFakeService.from_client(client,
384
{'message_id': 'foo', 'headers': {'From': 'me'}}
386
response = client.get_messages('baz', headers=[
387
'Subject', 'Date', 'X-Launchpad-Message-Rationale'])
388
first_message = response['messages'][0]
389
self.assertEqual('foo', first_message['message_id'])
390
self.assertEqual({}, first_message['headers'])
392
def test_get_messages_headers_include_headers(self):
393
client = GrackleClient('localhost', 8442)
394
with ForkedFakeService.from_client(client,
396
{'message_id': 'foo', 'headers': {'From': 'me', 'To': 'you'}}
398
response = client.get_messages('baz', headers=[
400
first_message = response['messages'][0]
401
self.assertEqual('foo', first_message['message_id'])
402
self.assertEqual({'From': 'me', 'To': 'you'}, first_message['headers'])
404
def test_get_messages_max_body_length(self):
405
client = GrackleClient('localhost', 8443)
406
with ForkedFakeService.from_client(client,
408
{'message_id': 'foo', 'body': u'abcdefghi'}
410
response = client.get_messages('baz', max_body_length=3)
411
first_message = response['messages'][0]
412
self.assertEqual('abc', first_message['body'])
414
def test_include_hidden(self):
415
client = GrackleClient('localhost', 8444)
416
with ForkedFakeService.from_client(client,
418
{'message_id': 'foo', 'hidden': True},
419
{'message_id': 'bar', 'hidden': False}
421
response = client.get_messages('baz', include_hidden=True)
422
self.assertMessageIDs(['bar', 'foo'], response['messages'])
423
response = client.get_messages('baz', include_hidden=False)
424
self.assertMessageIDs(['bar'], response['messages'])
426
def test_display_type_unknown_value(self):
427
client = GrackleClient('localhost', 8445)
428
with ForkedFakeService.from_client(client,
430
{'message_id': 'foo', 'body': u'abcdefghi'}
432
with ExpectedException(UnsupportedDisplayType, ''):
433
client.get_messages('baz', display_type='unknown')
435
def test_display_type_headers_only(self):
436
client = GrackleClient('localhost', 8445)
437
with ForkedFakeService.from_client(client,
439
{'message_id': 'foo',
440
'headers': {'From': 'me', 'To': 'you'},
443
response = client.get_messages('baz', display_type='headers-only')
444
first_message = response['messages'][0]
445
self.assertEqual('foo', first_message['message_id'])
446
self.assertEqual({'From': 'me', 'To': 'you'}, first_message['headers'])
447
self.assertNotIn('body', first_message)