3
3
BaseHTTPRequestHandler,
5
from email.message import Message
6
from email.mime.multipart import MIMEMultipart
7
from email.mime.text import MIMEText
11
7
from signal import SIGKILL
13
8
from StringIO import StringIO
15
9
from unittest import TestCase
16
from urlparse import urlparse
17
from urlparse import parse_qs
19
11
from testtools import ExpectedException
21
13
from grackle.client import (
24
UnsupportedDisplayType,
29
def make_message(message_id, body='body', headers=None, hidden=False):
32
headers['Message-Id'] = message_id
34
'message_id': message_id,
36
'thread_id': message_id,
37
'date': headers.get('date', '2005-01-01'),
38
'subject': headers.get('subject', 'subject'),
39
'author': headers.get('author', 'author'),
42
'replies': headers.get('in-reply-to', None),
48
def make_mime_message(message_id, body='body', headers=None, hidden=False,
49
attachment_type=None):
50
message = MIMEMultipart()
51
message.attach(MIMEText(body))
52
if attachment_type is not None:
53
attachment = Message()
54
attachment.set_payload('attactment data.')
55
attachment['Content-Type'] = attachment_type
56
attachment['Content-Disposition'] = 'attachment; filename="file.ext"'
57
message.attach(attachment)
58
return make_message(message_id, message.get_payload(), headers, hidden)
61
def threaded_messages(messages):
65
for message in messages:
66
if message.get('replies') is None:
67
threads[message['message_id']] = [message]
70
pending.append(message)
71
for message in pending:
72
threads[message['replies']].append(message)
73
return threads.values()
77
"""A memory-backed message store."""
79
def __init__(self, messages):
81
self.messages = messages
84
def is_multipart(message):
85
return isinstance(message['body'], list)
87
def get_messages(self, archive_id, query_string):
88
"""Return matching messages.
90
:param archive_id: The archive to retrieve from.
91
:param query_string: Contains 'parameters', which is a JSON-format
92
string describing parameters.
94
query = parse_qs(query_string)
95
parameters = simplejson.loads(query['parameters'][0])
96
order = parameters.get('order')
97
messages = self.messages[archive_id]
99
if order not in SUPPORTED_ORDERS:
100
raise UnsupportedOrder
101
elif order.startswith('thread_'):
102
threaded = threaded_messages(messages)
104
if order == 'thread_subject':
105
threaded.sort(key=lambda t: t[0]['subject'])
106
if order == 'thread_oldest':
107
threaded.sort(key=lambda t: min(m['date'] for m in t))
108
if order == 'thread_newest':
109
threaded.sort(key=lambda t: max(m['date'] for m in t))
110
for thread in threaded:
111
messages.extend(thread)
113
messages.sort(key=lambda m: m[order])
114
display_type = parameters.get('display_type', 'all')
115
if display_type not in SUPPORTED_DISPLAY_TYPES:
116
raise UnsupportedDisplayType
117
if 'date_range' in parameters:
119
start_date, end_date = parameters['date_range'].split('..')
121
raise UnparsableDateRange
123
for message in messages:
124
if (not parameters['include_hidden'] and message['hidden']):
126
if ('message_ids' in parameters
127
and message['message_id'] not in parameters['message_ids']):
129
if ('date_range' in parameters
130
and (message['date'] < start_date
131
or message['date'] > end_date)):
133
message = dict(message)
134
if 'headers' in parameters:
136
(k, v) for k, v in message['headers'].iteritems()
137
if k in parameters['headers'])
138
message['headers'] = headers
139
if display_type == 'headers-only':
141
elif display_type == 'text-only' and self.is_multipart(message):
143
part.get_payload() for part in message['body']
144
if part.get_content_type() == 'text/plain']
145
message['body'] = '\n\n'.join(text_parts)
146
elif display_type == 'all' and self.is_multipart(message):
147
parts = [str(part.get_payload()) for part in message['body']]
148
message['body'] = '\n\n'.join(parts)
149
max_body = parameters.get('max_body_length')
150
if max_body is not None and display_type != 'headers-only':
151
message['body'] = message['body'][:max_body]
152
new_messages.append(message)
153
messages = new_messages
154
limit = parameters.get('limit', 100)
155
memo = parameters.get('memo')
156
message_id_indices = dict(
157
(m['message_id'], idx) for idx, m in enumerate(messages))
161
start = message_id_indices[memo.encode('rot13')]
163
previous_memo = messages[start - 1]['message_id'].encode('rot13')
166
end = min(start + limit, len(messages))
167
if end < len(messages):
168
next_memo = messages[end]['message_id'].encode('rot13')
171
messages = messages[start:end]
174
'messages': messages,
175
'next_memo': next_memo,
176
'previous_memo': previous_memo
181
class ForkedFakeService:
182
"""A Grackle service fake, as a ContextManager."""
184
def __init__(self, port, messages=None, write_logs=False):
187
:param port: The tcp port to use.
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.
191
:param write_logs: If true, log messages will be written to stdout.
20
def __init__(self, func_or_method, *args):
21
self.func_or_method = func_or_method
198
self.messages = messages
199
self.read_end, self.write_end = os.pipe()
200
self.write_logs = write_logs
203
def from_client(client, messages=None):
204
"""Instantiate a ForkedFakeService from the client.
206
:param port: The client to provide service for.
207
:param messages: A dict of lists of dicts representing messages. The
208
outer dict represents the archive, the list represents the list of
209
messages for that archive.
211
return ForkedFakeService(client.port, messages)
214
"""Tell the parent process that the server is ready for writes."""
215
os.write(self.write_end, 'asdf')
217
25
def __enter__(self):
220
Fork and start a server in the child. Return when the server is ready
226
os.read(self.read_end, 1)
30
self.func_or_method(*self.args)
229
def start_server(self):
230
"""Start the HTTP server."""
231
service = HTTPServer(('', self.port), FakeGrackleRequestHandler)
232
service.store = GrackleStore(self.messages)
233
for archive_id, messages in service.store.messages.iteritems():
234
for message in messages:
235
message.setdefault('headers', {})
239
stream=sys.stderr, level=logging.INFO)
240
service.serve_forever()
242
33
def __exit__(self, exc_type, exc_val, traceback):
243
34
os.kill(self.pid, SIGKILL)
246
SUPPORTED_DISPLAY_TYPES = set(['all', 'text-only', 'headers-only'])
249
SUPPORTED_ORDERS = set(
250
['date', 'author', 'subject', 'thread_newest', 'thread_oldest',
254
37
class FakeGrackleRequestHandler(BaseHTTPRequestHandler):
255
"""A request handler that forwards to server.store."""
257
def __init__(self, *args, **kwargs):
258
"""Constructor. Sets up logging."""
259
self.logger = logging.getLogger('http')
260
BaseHTTPRequestHandler.__init__(self, *args, **kwargs)
262
39
def do_POST(self):
263
"""Create a message on POST."""
264
40
message = self.rfile.read(int(self.headers['content-length']))
265
41
if message == 'This is a message':
266
42
self.send_response(httplib.CREATED)
307
73
class TestGetMessages(TestCase):
309
def assertIDOrder(self, ids, messages):
310
self.assertEqual(ids, [m['message_id'] for m in messages])
312
def assertMessageIDs(self, ids, messages):
314
sorted(ids), sorted(messages, key=lambda m: m['message_id']))
316
75
def test_get_messages(self):
317
76
client = GrackleClient('localhost', 8435)
319
'baz': [make_message('foo'), make_message('bar')]}
320
with ForkedFakeService.from_client(client, archive):
321
response = client.get_messages('baz')
322
self.assertEqual(['bar', 'foo'], sorted(m['message_id'] for m in
323
response['messages']))
324
self.assertIs(None, response['next_memo'])
325
self.assertIs(None, response['previous_memo'])
327
def test_get_messages_by_id(self):
328
client = GrackleClient('localhost', 8437)
330
'baz': [make_message('foo'), make_message('bar')]}
331
with ForkedFakeService.from_client(client, archive):
332
response = client.get_messages('baz', message_ids=['foo'])
333
message, = response['messages']
334
self.assertEqual('foo', message['message_id'])
336
def test_get_messages_batching(self):
337
client = GrackleClient('localhost', 8438)
338
archive = {'baz': [make_message('foo'), make_message('bar')]}
339
with ForkedFakeService.from_client(client, archive):
340
response = client.get_messages('baz', limit=1)
341
self.assertEqual(1, len(response['messages']))
342
messages = response['messages']
343
response = client.get_messages(
344
'baz', limit=1, memo=response['next_memo'])
345
self.assertEqual(1, len(response['messages']))
346
messages.extend(response['messages'])
347
self.assertMessageIDs(['foo', 'bar'], messages)
349
def get_messages_member_order_test(self, key):
350
client = GrackleClient('localhost', 8439)
353
make_message('foo', headers={key: '2011-03-25'}),
354
make_message('bar', headers={key: '2011-03-24'}),
356
with ForkedFakeService.from_client(client, archive):
357
response = client.get_messages('baz')
358
self.assertIDOrder(['foo', 'bar'], response['messages'])
359
response = client.get_messages('baz', order=key)
360
self.assertIDOrder(['bar', 'foo'], response['messages'])
362
def test_get_messages_date_order(self):
363
self.get_messages_member_order_test('date')
365
def test_get_messages_author_order(self):
366
self.get_messages_member_order_test('author')
368
def test_get_messages_subject_order(self):
369
self.get_messages_member_order_test('subject')
371
def test_get_messages_thread_subject_order(self):
374
make_message('bar', headers={'subject': 'y'}),
375
make_message('qux', headers={'subject': 'z'}),
376
make_message('foo', headers={'subject': 'x',
377
'in-reply-to': 'qux'}),
379
client = GrackleClient('localhost', 8439)
380
with ForkedFakeService.from_client(client, archive):
381
response = client.get_messages('baz')
382
self.assertIDOrder(['bar', 'qux', 'foo'], response['messages'])
383
response = client.get_messages('baz', order='subject')
384
self.assertIDOrder(['foo', 'bar', 'qux'], response['messages'])
385
response = client.get_messages('baz', order='thread_subject')
386
self.assertIDOrder(['bar', 'qux', 'foo'], response['messages'])
388
def test_get_messages_thread_oldest_order(self):
389
client = GrackleClient('localhost', 8439)
392
make_message('bar', headers={'date': 'x'}),
393
make_message('qux', headers={'date': 'z'}),
394
make_message('foo', headers={'date': 'y',
395
'in-reply-to': 'qux'}),
397
with ForkedFakeService.from_client(client, archive):
398
response = client.get_messages('baz')
399
self.assertIDOrder(['bar', 'qux', 'foo'], response['messages'])
400
response = client.get_messages('baz', order='date')
401
self.assertIDOrder(['bar', 'foo', 'qux'], response['messages'])
402
response = client.get_messages('baz', order='thread_oldest')
403
self.assertIDOrder(['bar', 'qux', 'foo'], response['messages'])
405
def test_get_messages_thread_newest_order(self):
406
client = GrackleClient('localhost', 8439)
409
make_message('bar', headers={'date': 'x'}),
410
make_message('qux', headers={'date': 'w'}),
411
make_message('foo', headers={'date': 'y',
412
'in-reply-to': 'bar'}),
413
make_message('baz', headers={'date': 'z',
414
'in-reply-to': 'qux'}),
416
with ForkedFakeService.from_client(client, archive):
417
response = client.get_messages('baz', order='date')
419
['qux', 'bar', 'foo', 'baz'], response['messages'])
420
response = client.get_messages('baz', order='thread_newest')
422
['bar', 'foo', 'qux', 'baz'], response['messages'])
424
def test_get_messages_unsupported_order(self):
425
client = GrackleClient('localhost', 8439)
428
make_message('foo', headers={'date': '2011-03-25'}),
429
make_message('foo', headers={'date': '2011-03-24'}),
431
with ForkedFakeService.from_client(client, archive):
432
with ExpectedException(UnsupportedOrder, ''):
433
client.get_messages('baz', order='nonsense')
435
def test_get_messages_headers_no_headers(self):
436
client = GrackleClient('localhost', 8440)
437
archive = {'baz': [make_message('foo')]}
438
with ForkedFakeService.from_client(client, archive):
439
response = client.get_messages('baz', headers=[
440
'Subject', 'Date', 'X-Launchpad-Message-Rationale'])
441
first_message = response['messages'][0]
442
self.assertEqual('foo', first_message['message_id'])
443
self.assertEqual({}, first_message['headers'])
445
def test_get_messages_headers_exclude_headers(self):
446
client = GrackleClient('localhost', 8441)
448
'baz': [make_message('foo', headers={'From': 'me'})]}
449
with ForkedFakeService.from_client(client, archive):
450
response = client.get_messages('baz', headers=[
451
'Subject', 'Date', 'X-Launchpad-Message-Rationale'])
452
first_message = response['messages'][0]
453
self.assertEqual('foo', first_message['message_id'])
454
self.assertEqual({}, first_message['headers'])
456
def test_get_messages_headers_include_headers(self):
457
client = GrackleClient('localhost', 8442)
460
make_message('foo', headers={'From': 'me', 'To': 'you'})]}
461
with ForkedFakeService.from_client(client, archive):
462
response = client.get_messages('baz', headers=[
464
first_message = response['messages'][0]
465
self.assertEqual('foo', first_message['message_id'])
466
self.assertEqual({'From': 'me', 'To': 'you'}, first_message['headers'])
468
def test_get_messages_max_body_length(self):
469
client = GrackleClient('localhost', 8443)
470
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
471
with ForkedFakeService.from_client(client, archive):
472
response = client.get_messages('baz', max_body_length=3)
473
first_message = response['messages'][0]
474
self.assertEqual('abc', first_message['body'])
476
def test_include_hidden(self):
477
client = GrackleClient('localhost', 8444)
480
make_message('foo', hidden=True),
481
make_message('bar', hidden=False),
483
with ForkedFakeService.from_client(client, archive):
484
response = client.get_messages('baz', include_hidden=True)
485
self.assertMessageIDs(['bar', 'foo'], response['messages'])
486
response = client.get_messages('baz', include_hidden=False)
487
self.assertMessageIDs(['bar'], response['messages'])
489
def test_display_type_unknown_value(self):
490
client = GrackleClient('localhost', 8445)
491
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
492
with ForkedFakeService.from_client(client, archive):
493
with ExpectedException(UnsupportedDisplayType, ''):
494
client.get_messages('baz', display_type='unknown')
496
def test_display_type_headers_only(self):
497
client = GrackleClient('localhost', 8446)
500
make_message('foo', body=u'abcdefghi',
501
headers={'From': 'me', 'To': 'you'})]}
502
with ForkedFakeService.from_client(client, archive):
503
response = client.get_messages('baz', display_type='headers-only')
504
first_message = response['messages'][0]
505
self.assertEqual('foo', first_message['message_id'])
507
{'From': 'me', 'Message-Id': 'foo', 'To': 'you'},
508
first_message['headers'])
509
self.assertNotIn('body', first_message)
511
def test_display_type_text_only(self):
512
client = GrackleClient('localhost', 8446)
517
headers={'From': 'me', 'To': 'you'},
518
attachment_type='text/x-diff')]}
519
with ForkedFakeService.from_client(client, archive):
520
response = client.get_messages('baz', display_type='text-only')
521
first_message = response['messages'][0]
522
self.assertEqual('foo', first_message['message_id'])
523
self.assertEqual('me', first_message['headers']['From'])
524
self.assertEqual('you', first_message['headers']['To'])
525
self.assertEqual('abcdefghi', first_message['body'])
527
def test_display_type_all(self):
528
client = GrackleClient('localhost', 8447)
533
headers={'From': 'me', 'To': 'you'},
534
attachment_type='text/x-diff')]}
535
with ForkedFakeService.from_client(client, archive):
536
response = client.get_messages('baz', display_type='all')
537
first_message = response['messages'][0]
538
self.assertEqual('foo', first_message['message_id'])
539
self.assertEqual('me', first_message['headers']['From'])
540
self.assertEqual('you', first_message['headers']['To'])
542
'abcdefghi\n\nattactment data.', first_message['body'])
544
def test_date_range(self):
545
client = GrackleClient('localhost', 8448)
549
'foo', 'abcdefghi', headers={'date': '2011-12-31'}),
551
'bar', 'abcdefghi', headers={'date': '2012-01-01'}),
553
'qux', 'abcdefghi', headers={'date': '2012-01-15'}),
555
'naf', 'abcdefghi', headers={'date': '2012-01-31'}),
557
'doh', 'abcdefghi', headers={'date': '2012-02-01'}),
559
with ForkedFakeService.from_client(client, archive):
560
response = client.get_messages(
561
'baz', date_range='2012-01-01..2012-01-31')
562
ids = sorted(m['message_id'] for m in response['messages'])
563
self.assertEqual(['bar', 'naf', 'qux'], ids)
565
def test_date_range_unparsabledaterange(self):
566
client = GrackleClient('localhost', 8448)
567
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
568
with ForkedFakeService.from_client(client, archive):
569
with ExpectedException(UnparsableDateRange, ''):
570
client.get_messages('baz', date_range='2012-01-01')
77
with fake_grackle_service(client,
79
[{'message-id': 'foo'},
80
{'message-id': 'bar'}]}):
81
response = client.get_messages('baz')
82
self.assertEqual(['bar', 'foo'], sorted(response.keys()))