3
3
BaseHTTPRequestHandler,
5
from email.message import Message
6
from email.mime.multipart import MIMEMultipart
7
from email.mime.text import MIMEText
7
11
from signal import SIGKILL
8
13
from StringIO import StringIO
9
15
from unittest import TestCase
16
from urlparse import urlparse
17
from urlparse import parse_qs
11
19
from testtools import ExpectedException
13
from grackle import client
18
def __init__(self, func_or_method):
19
self.func_or_method = func_or_method
21
from grackle.client import (
23
UnsupportedDisplayType,
28
def make_message(message_id, text, headers=None, attachment_type=None):
29
message = MIMEMultipart()
30
message.attach(MIMEText(text))
31
message['Message-Id'] = message_id
32
if headers is not None:
33
for k, v in headers.items():
35
if attachment_type is not None:
36
attachment = Message()
37
attachment.set_payload('attactment data.')
38
attachment['Content-Type'] = attachment_type
39
attachment['Content-Disposition'] = (
40
'attachment; filename="file.ext"')
41
message.attach(attachment)
43
'message_id': message_id,
44
'headers': dict(message.items()),
45
'body': message.get_payload(),
50
def threaded_messages(messages):
54
for message in messages:
55
if message.get('in_reply_to') is None:
56
threads[message['message_id']] = [message]
59
pending.append(message)
60
for message in pending:
61
threads[message['in_reply_to']].append(message)
62
return threads.values()
66
"""A memory-backed message store."""
68
def __init__(self, messages):
70
self.messages = messages
72
def get_messages(self, archive_id, query_string):
73
"""Return matching messages.
75
:param archive_id: The archive to retrieve from.
76
:param query_string: Contains 'parameters', which is a JSON-format
77
string describing parameters.
79
query = parse_qs(query_string)
80
parameters = simplejson.loads(query['parameters'][0])
81
order = parameters.get('order')
82
messages = self.messages[archive_id]
84
if order not in SUPPORTED_ORDERS:
85
raise UnsupportedOrder
86
elif order.startswith('thread_'):
87
threaded = threaded_messages(messages)
89
if order == 'thread_subject':
90
threaded.sort(key=lambda t: t[0]['subject'])
91
if order == 'thread_oldest':
92
threaded.sort(key=lambda t: min(m['date'] for m in t))
93
if order == 'thread_newest':
94
threaded.sort(key=lambda t: max(m['date'] for m in t))
95
for thread in threaded:
96
messages.extend(thread)
98
messages.sort(key=lambda m: m[order])
99
display_type = parameters.get('display_type', 'all')
100
if display_type not in SUPPORTED_DISPLAY_TYPES:
101
raise UnsupportedDisplayType
103
for message in messages:
104
if (not parameters['include_hidden']
105
and message.get('hidden', False)):
108
if ('message_ids' in parameters
109
and message['message_id'] not in parameters['message_ids']):
111
message = dict(message)
112
if 'headers' in parameters:
114
(k, v) for k, v in message['headers'].iteritems()
115
if k in parameters['headers'])
116
message['headers'] = headers
117
max_body = parameters.get('max_body_length')
118
if display_type == 'headers-only':
120
elif (display_type == 'text-only'
121
and isinstance(message['body'], list)):
123
part.get_payload() for part in message['body']
124
if part.get_content_type() == 'text/plain']
125
message['body'] = '\n\n'.join(text_parts)
126
elif (display_type == 'all'
127
and 'body' in message
128
and isinstance(message['body'], list)):
129
parts = [str(part.get_payload()) for part in message['body']]
130
message['body'] = '\n\n'.join(parts)
131
if max_body is not None and display_type != 'headers-only':
132
message['body'] = message['body'][:max_body]
133
new_messages.append(message)
134
messages = new_messages
135
limit = parameters.get('limit', 100)
136
memo = parameters.get('memo')
137
message_id_indices = dict(
138
(m['message_id'], idx) for idx, m in enumerate(messages))
142
start = message_id_indices[memo.encode('rot13')]
144
previous_memo = messages[start - 1]['message_id'].encode('rot13')
147
end = min(start + limit, len(messages))
148
if end < len(messages):
149
next_memo = messages[end]['message_id'].encode('rot13')
152
messages = messages[start:end]
155
'messages': messages,
156
'next_memo': next_memo,
157
'previous_memo': previous_memo
162
class ForkedFakeService:
163
"""A Grackle service fake, as a ContextManager."""
165
def __init__(self, port, messages=None, write_logs=False):
168
:param port: The tcp port to use.
169
:param messages: A dict of lists of dicts representing messages. The
170
outer dict represents the archive, the list represents the list of
171
messages for that archive.
172
:param write_logs: If true, log messages will be written to stdout.
179
self.messages = messages
180
self.read_end, self.write_end = os.pipe()
181
self.write_logs = write_logs
184
def from_client(client, messages=None):
185
"""Instantiate a ForkedFakeService from the client.
187
:param port: The client to provide service for.
188
:param messages: A dict of lists of dicts representing messages. The
189
outer dict represents the archive, the list represents the list of
190
messages for that archive.
192
return ForkedFakeService(client.port, messages)
195
"""Tell the parent process that the server is ready for writes."""
196
os.write(self.write_end, 'asdf')
22
198
def __enter__(self):
201
Fork and start a server in the child. Return when the server is ready
207
os.read(self.read_end, 1)
210
def start_server(self):
211
"""Start the HTTP server."""
212
service = HTTPServer(('', self.port), FakeGrackleRequestHandler)
213
service.store = GrackleStore(self.messages)
214
for archive_id, messages in service.store.messages.iteritems():
215
for message in messages:
216
message.setdefault('headers', {})
220
stream=sys.stderr, level=logging.INFO)
221
service.serve_forever()
30
223
def __exit__(self, exc_type, exc_val, traceback):
31
224
os.kill(self.pid, SIGKILL)
227
SUPPORTED_DISPLAY_TYPES = set(['all', 'text-only', 'headers-only'])
230
SUPPORTED_ORDERS = set(
231
['date', 'author', 'subject', 'thread_newest', 'thread_oldest',
34
235
class FakeGrackleRequestHandler(BaseHTTPRequestHandler):
236
"""A request handler that forwards to server.store."""
238
def __init__(self, *args, **kwargs):
239
"""Constructor. Sets up logging."""
240
self.logger = logging.getLogger('http')
241
BaseHTTPRequestHandler.__init__(self, *args, **kwargs)
36
243
def do_POST(self):
244
"""Create a message on POST."""
37
245
message = self.rfile.read(int(self.headers['content-length']))
38
246
if message == 'This is a message':
39
247
self.send_response(httplib.CREATED)
43
251
self.send_error(httplib.BAD_REQUEST)
47
service = HTTPServer(('', 8435), FakeGrackleRequestHandler)
48
service.serve_forever()
254
"""Retrieve a list of messages on GET."""
255
scheme, netloc, path, params, query_string, fragments = (
257
parts = path.split('/')
258
if parts[1] == 'archive':
260
response = self.server.store.get_messages(
261
parts[2], query_string)
262
self.send_response(httplib.OK)
264
self.wfile.write(simplejson.dumps(response))
265
except UnsupportedOrder:
267
httplib.BAD_REQUEST, UnsupportedOrder.__doc__)
269
except UnsupportedDisplayType:
271
httplib.BAD_REQUEST, UnsupportedDisplayType.__doc__)
274
def log_message(self, format, *args):
275
"""Override log_message to use standard Python logging."""
276
message = "%s - - [%s] %s\n" % (
277
self.address_string(), self.log_date_time_string(), format % args)
278
self.logger.info(message)
52
281
class TestPutMessage(TestCase):
54
283
def test_put_message(self):
55
with Forked(run_service):
56
client.put_message('arch1', StringIO('This is a message'))
284
client = GrackleClient('localhost', 8436)
285
with ForkedFakeService.from_client(client):
286
client.put_message('arch1', 'asdf', StringIO('This is a message'))
57
287
with ExpectedException(Exception, 'wtf'):
58
client.put_message('arch1', StringIO('This is not a message'))
288
client.put_message('arch1', 'asdf',
289
StringIO('This is not a message'))
292
class TestGetMessages(TestCase):
294
def assertIDOrder(self, ids, messages):
295
self.assertEqual(ids, [m['message_id'] for m in messages])
297
def assertMessageIDs(self, ids, messages):
299
sorted(ids), sorted(messages, key=lambda m: m['message_id']))
301
def test_get_messages(self):
302
client = GrackleClient('localhost', 8435)
303
with ForkedFakeService.from_client(client,
305
[{'message_id': 'foo'},
306
{'message_id': 'bar'}]}):
307
response = client.get_messages('baz')
308
self.assertEqual(['bar', 'foo'], sorted(m['message_id'] for m in
309
response['messages']))
310
self.assertIs(None, response['next_memo'])
311
self.assertIs(None, response['previous_memo'])
313
def test_get_messages_by_id(self):
314
client = GrackleClient('localhost', 8437)
315
with ForkedFakeService.from_client(client,
317
[{'message_id': 'foo'},
318
{'message_id': 'bar'}]}):
319
response = client.get_messages('baz', message_ids=['foo'])
320
message, = response['messages']
321
self.assertEqual('foo', message['message_id'])
323
def test_get_messages_batching(self):
324
client = GrackleClient('localhost', 8438)
325
with ForkedFakeService.from_client(client,
327
[{'message_id': 'foo'},
328
{'message_id': 'bar'}]}):
329
response = client.get_messages('baz', limit=1)
330
self.assertEqual(1, len(response['messages']))
331
messages = response['messages']
332
response = client.get_messages(
333
'baz', limit=1, memo=response['next_memo'])
334
self.assertEqual(1, len(response['messages']))
335
messages.extend(response['messages'])
336
self.assertMessageIDs(['foo', 'bar'], messages)
338
def get_messages_member_order_test(self, key):
339
client = GrackleClient('localhost', 8439)
340
with ForkedFakeService.from_client(client,
341
{'baz': [{'message_id': 'foo', key: '2011-03-25'},
342
{'message_id': 'bar', key: '2011-03-24'}]}):
343
response = client.get_messages('baz')
344
self.assertIDOrder(['foo', 'bar'], response['messages'])
345
response = client.get_messages('baz', order=key)
346
self.assertIDOrder(['bar', 'foo'], response['messages'])
348
def test_get_messages_date_order(self):
349
self.get_messages_member_order_test('date')
351
def test_get_messages_author_order(self):
352
self.get_messages_member_order_test('author')
354
def test_get_messages_subject_order(self):
355
self.get_messages_member_order_test('subject')
357
def test_get_messages_thread_subject_order(self):
358
client = GrackleClient('localhost', 8439)
359
with ForkedFakeService.from_client(client, {'baz': [
360
{'message_id': 'bar', 'subject': 'y'},
361
{'message_id': 'qux', 'subject': 'z'},
362
{'message_id': 'foo', 'subject': 'x', 'in_reply_to': 'qux'},
364
response = client.get_messages('baz')
365
self.assertIDOrder(['bar', 'qux', 'foo'], response['messages'])
366
response = client.get_messages('baz', order='subject')
367
self.assertIDOrder(['foo', 'bar', 'qux'], response['messages'])
368
response = client.get_messages('baz', order='thread_subject')
369
self.assertIDOrder(['bar', 'qux', 'foo'], response['messages'])
371
def test_get_messages_thread_oldest_order(self):
372
client = GrackleClient('localhost', 8439)
373
with ForkedFakeService.from_client(client, {'baz': [
374
{'message_id': 'bar', 'date': 'x'},
375
{'message_id': 'qux', 'date': 'z'},
376
{'message_id': 'foo', 'date': 'y', 'in_reply_to': 'qux'},
378
response = client.get_messages('baz')
379
self.assertIDOrder(['bar', 'qux', 'foo'], response['messages'])
380
response = client.get_messages('baz', order='date')
381
self.assertIDOrder(['bar', 'foo', 'qux'], response['messages'])
382
response = client.get_messages('baz', order='thread_oldest')
383
self.assertIDOrder(['bar', 'qux', 'foo'], response['messages'])
385
def test_get_messages_thread_newest_order(self):
386
client = GrackleClient('localhost', 8439)
387
with ForkedFakeService.from_client(client, {'baz': [
388
{'message_id': 'bar', 'date': 'x'},
389
{'message_id': 'qux', 'date': 'w'},
390
{'message_id': 'foo', 'date': 'y', 'in_reply_to': 'bar'},
391
{'message_id': 'baz', 'date': 'z', 'in_reply_to': 'qux'},
393
response = client.get_messages('baz', order='date')
395
['qux', 'bar', 'foo', 'baz'], response['messages'])
396
response = client.get_messages('baz', order='thread_newest')
398
['bar', 'foo', 'qux', 'baz'], response['messages'])
400
def test_get_messages_unsupported_order(self):
401
client = GrackleClient('localhost', 8439)
402
with ForkedFakeService.from_client(client,
403
{'baz': [{'message_id': 'foo', 'date': '2011-03-25'},
404
{'message_id': 'bar', 'date': '2011-03-24'}]}):
405
with ExpectedException(UnsupportedOrder, ''):
406
client.get_messages('baz', order='nonsense')
408
def test_get_messages_headers_no_headers(self):
409
client = GrackleClient('localhost', 8440)
410
with ForkedFakeService.from_client(client,
412
{'message_id': 'foo'}
414
response = client.get_messages('baz', headers=[
415
'Subject', 'Date', 'X-Launchpad-Message-Rationale'])
416
first_message = response['messages'][0]
417
self.assertEqual('foo', first_message['message_id'])
418
self.assertEqual({}, first_message['headers'])
420
def test_get_messages_headers_exclude_headers(self):
421
client = GrackleClient('localhost', 8441)
422
with ForkedFakeService.from_client(client,
424
{'message_id': 'foo', 'headers': {'From': 'me'}}
426
response = client.get_messages('baz', headers=[
427
'Subject', 'Date', 'X-Launchpad-Message-Rationale'])
428
first_message = response['messages'][0]
429
self.assertEqual('foo', first_message['message_id'])
430
self.assertEqual({}, first_message['headers'])
432
def test_get_messages_headers_include_headers(self):
433
client = GrackleClient('localhost', 8442)
434
with ForkedFakeService.from_client(client,
436
{'message_id': 'foo', 'headers': {'From': 'me', 'To': 'you'}}
438
response = client.get_messages('baz', headers=[
440
first_message = response['messages'][0]
441
self.assertEqual('foo', first_message['message_id'])
442
self.assertEqual({'From': 'me', 'To': 'you'}, first_message['headers'])
444
def test_get_messages_max_body_length(self):
445
client = GrackleClient('localhost', 8443)
446
with ForkedFakeService.from_client(client,
448
{'message_id': 'foo', 'body': u'abcdefghi'}
450
response = client.get_messages('baz', max_body_length=3)
451
first_message = response['messages'][0]
452
self.assertEqual('abc', first_message['body'])
454
def test_include_hidden(self):
455
client = GrackleClient('localhost', 8444)
456
with ForkedFakeService.from_client(client,
458
{'message_id': 'foo', 'hidden': True},
459
{'message_id': 'bar', 'hidden': False}
461
response = client.get_messages('baz', include_hidden=True)
462
self.assertMessageIDs(['bar', 'foo'], response['messages'])
463
response = client.get_messages('baz', include_hidden=False)
464
self.assertMessageIDs(['bar'], response['messages'])
466
def test_display_type_unknown_value(self):
467
client = GrackleClient('localhost', 8445)
468
with ForkedFakeService.from_client(client,
470
{'message_id': 'foo', 'body': u'abcdefghi'}
472
with ExpectedException(UnsupportedDisplayType, ''):
473
client.get_messages('baz', display_type='unknown')
475
def test_display_type_headers_only(self):
476
client = GrackleClient('localhost', 8446)
477
with ForkedFakeService.from_client(client,
479
{'message_id': 'foo',
480
'headers': {'From': 'me', 'To': 'you'},
483
response = client.get_messages('baz', display_type='headers-only')
484
first_message = response['messages'][0]
485
self.assertEqual('foo', first_message['message_id'])
486
self.assertEqual({'From': 'me', 'To': 'you'}, first_message['headers'])
487
self.assertNotIn('body', first_message)
489
def test_display_type_text_only(self):
490
client = GrackleClient('localhost', 8446)
491
with ForkedFakeService.from_client(client,
495
headers={'From': 'me', 'To': 'you'},
496
attachment_type='text/x-diff')
498
response = client.get_messages('baz', display_type='text-only')
499
first_message = response['messages'][0]
500
self.assertEqual('foo', first_message['message_id'])
501
self.assertEqual('me', first_message['headers']['From'])
502
self.assertEqual('you', first_message['headers']['To'])
503
self.assertEqual('abcdefghi', first_message['body'])
505
def test_display_type_all(self):
506
client = GrackleClient('localhost', 8447)
507
with ForkedFakeService.from_client(client,
511
headers={'From': 'me', 'To': 'you'},
512
attachment_type='text/x-diff')
514
response = client.get_messages('baz', display_type='all')
515
first_message = response['messages'][0]
516
self.assertEqual('foo', first_message['message_id'])
517
self.assertEqual('me', first_message['headers']['From'])
518
self.assertEqual('you', first_message['headers']['To'])
520
'abcdefghi\n\nattactment data.', first_message['body'])