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
from grackle.client import (
23
UnsupportedDisplayType,
28
def make_json_message(text, message_id='foo',
29
headers=None, attachment_type=None):
30
message = MIMEMultipart()
31
message.attach(MIMEText(text))
32
message['Message-Id'] = message_id
33
if headers is not None:
34
for k, v in headers.items():
36
if attachment_type is not None:
37
attachment = Message()
38
attachment.set_payload('attactment data.')
39
attachment['Content-Type'] = attachment_type
40
attachment['Content-Disposition'] = (
41
'attachment; filename="file.ext"')
42
message.attach(attachment)
44
'message_id': message_id,
45
'headers': dict(message.items()),
46
'body': message.get_payload(),
51
def threaded_messages(messages):
55
for message in messages:
56
if message.get('in_reply_to') is None:
57
threads[message['message_id']] = [message]
60
pending.append(message)
61
for message in pending:
62
threads[message['in_reply_to']].append(message)
63
return threads.values()
67
"""A memory-backed message store."""
69
def __init__(self, messages):
71
self.messages = messages
73
def get_messages(self, archive_id, query_string):
74
"""Return matching messages.
76
:param archive_id: The archive to retrieve from.
77
:param query_string: Contains 'parameters', which is a JSON-format
78
string describing parameters.
80
query = parse_qs(query_string)
81
parameters = simplejson.loads(query['parameters'][0])
82
order = parameters.get('order')
83
messages = self.messages[archive_id]
85
if order not in SUPPORTED_ORDERS:
86
raise UnsupportedOrder
87
elif order.startswith('thread_'):
88
threaded = threaded_messages(messages)
90
if order == 'thread_subject':
91
threaded.sort(key=lambda t: t[0]['subject'])
92
if order == 'thread_oldest':
93
threaded.sort(key=lambda t: min(m['date'] for m in t))
94
if order == 'thread_newest':
95
threaded.sort(key=lambda t: max(m['date'] for m in t))
96
for thread in threaded:
97
messages.extend(thread)
99
messages.sort(key=lambda m: m[order])
100
display_type = parameters.get('display_type', 'all')
101
if display_type not in SUPPORTED_DISPLAY_TYPES:
102
raise UnsupportedDisplayType
104
for message in messages:
105
if (not parameters['include_hidden']
106
and message.get('hidden', False)):
109
if ('message_ids' in parameters
110
and message['message_id'] not in parameters['message_ids']):
112
message = dict(message)
113
if 'headers' in parameters:
115
(k, v) for k, v in message['headers'].iteritems()
116
if k in parameters['headers'])
117
message['headers'] = headers
118
max_body = parameters.get('max_body_length')
119
if display_type == 'headers-only':
121
elif (display_type == 'text-only'
122
and isinstance(message['body'], list)):
124
part.get_payload() for part in message['body']
125
if part.get_content_type() == 'text/plain']
126
message['body'] = '\n\n'.join(text_parts)
127
elif (display_type == 'all'
128
and 'body' in message
129
and isinstance(message['body'], list)):
130
parts = [str(part.get_payload()) for part in message['body']]
131
message['body'] = '\n\n'.join(parts)
132
if max_body is not None and display_type != 'headers-only':
133
message['body'] = message['body'][:max_body]
134
new_messages.append(message)
135
messages = new_messages
136
limit = parameters.get('limit', 100)
137
memo = parameters.get('memo')
138
message_id_indices = dict(
139
(m['message_id'], idx) for idx, m in enumerate(messages))
143
start = message_id_indices[memo.encode('rot13')]
145
previous_memo = messages[start - 1]['message_id'].encode('rot13')
148
end = min(start + limit, len(messages))
149
if end < len(messages):
150
next_memo = messages[end]['message_id'].encode('rot13')
153
messages = messages[start:end]
156
'messages': messages,
157
'next_memo': next_memo,
158
'previous_memo': previous_memo
163
class ForkedFakeService:
164
"""A Grackle service fake, as a ContextManager."""
166
def __init__(self, port, messages=None, write_logs=False):
169
:param port: The tcp port to use.
170
:param messages: A dict of lists of dicts representing messages. The
171
outer dict represents the archive, the list represents the list of
172
messages for that archive.
173
:param write_logs: If true, log messages will be written to stdout.
13
from grackle import client
18
def __init__(self, func_or_method):
19
self.func_or_method = func_or_method
180
self.messages = messages
181
self.read_end, self.write_end = os.pipe()
182
self.write_logs = write_logs
185
def from_client(client, messages=None):
186
"""Instantiate a ForkedFakeService from the client.
188
:param port: The client to provide service for.
189
:param messages: A dict of lists of dicts representing messages. The
190
outer dict represents the archive, the list represents the list of
191
messages for that archive.
193
return ForkedFakeService(client.port, messages)
196
"""Tell the parent process that the server is ready for writes."""
197
os.write(self.write_end, 'asdf')
199
22
def __enter__(self):
202
Fork and start a server in the child. Return when the server is ready
208
os.read(self.read_end, 1)
211
def start_server(self):
212
"""Start the HTTP server."""
213
service = HTTPServer(('', self.port), FakeGrackleRequestHandler)
214
service.store = GrackleStore(self.messages)
215
for archive_id, messages in service.store.messages.iteritems():
216
for message in messages:
217
message.setdefault('headers', {})
221
stream=sys.stderr, level=logging.INFO)
222
service.serve_forever()
224
30
def __exit__(self, exc_type, exc_val, traceback):
225
31
os.kill(self.pid, SIGKILL)
228
SUPPORTED_DISPLAY_TYPES = set(['all', 'text-only', 'headers-only'])
231
SUPPORTED_ORDERS = set(
232
['date', 'author', 'subject', 'thread_newest', 'thread_oldest',
236
34
class FakeGrackleRequestHandler(BaseHTTPRequestHandler):
237
"""A request handler that forwards to server.store."""
239
def __init__(self, *args, **kwargs):
240
"""Constructor. Sets up logging."""
241
self.logger = logging.getLogger('http')
242
BaseHTTPRequestHandler.__init__(self, *args, **kwargs)
244
36
def do_POST(self):
245
"""Create a message on POST."""
246
37
message = self.rfile.read(int(self.headers['content-length']))
247
38
if message == 'This is a message':
248
39
self.send_response(httplib.CREATED)
252
43
self.send_error(httplib.BAD_REQUEST)
255
"""Retrieve a list of messages on GET."""
256
scheme, netloc, path, params, query_string, fragments = (
258
parts = path.split('/')
259
if parts[1] == 'archive':
261
response = self.server.store.get_messages(
262
parts[2], query_string)
263
self.send_response(httplib.OK)
265
self.wfile.write(simplejson.dumps(response))
266
except UnsupportedOrder:
268
httplib.BAD_REQUEST, UnsupportedOrder.__doc__)
270
except UnsupportedDisplayType:
272
httplib.BAD_REQUEST, UnsupportedDisplayType.__doc__)
275
def log_message(self, format, *args):
276
"""Override log_message to use standard Python logging."""
277
message = "%s - - [%s] %s\n" % (
278
self.address_string(), self.log_date_time_string(), format % args)
279
self.logger.info(message)
47
service = HTTPServer(('', 8435), FakeGrackleRequestHandler)
48
service.serve_forever()
282
52
class TestPutMessage(TestCase):
284
54
def test_put_message(self):
285
client = GrackleClient('localhost', 8436)
286
with ForkedFakeService.from_client(client):
287
client.put_message('arch1', 'asdf', StringIO('This is a message'))
55
with Forked(run_service):
56
client.put_message('arch1', StringIO('This is a message'))
288
57
with ExpectedException(Exception, 'wtf'):
289
client.put_message('arch1', 'asdf',
290
StringIO('This is not a message'))
293
class TestGetMessages(TestCase):
295
def assertIDOrder(self, ids, messages):
296
self.assertEqual(ids, [m['message_id'] for m in messages])
298
def assertMessageIDs(self, ids, messages):
300
sorted(ids), sorted(messages, key=lambda m: m['message_id']))
302
def test_get_messages(self):
303
client = GrackleClient('localhost', 8435)
304
with ForkedFakeService.from_client(client,
306
[{'message_id': 'foo'},
307
{'message_id': 'bar'}]}):
308
response = client.get_messages('baz')
309
self.assertEqual(['bar', 'foo'], sorted(m['message_id'] for m in
310
response['messages']))
311
self.assertIs(None, response['next_memo'])
312
self.assertIs(None, response['previous_memo'])
314
def test_get_messages_by_id(self):
315
client = GrackleClient('localhost', 8437)
316
with ForkedFakeService.from_client(client,
318
[{'message_id': 'foo'},
319
{'message_id': 'bar'}]}):
320
response = client.get_messages('baz', message_ids=['foo'])
321
message, = response['messages']
322
self.assertEqual('foo', message['message_id'])
324
def test_get_messages_batching(self):
325
client = GrackleClient('localhost', 8438)
326
with ForkedFakeService.from_client(client,
328
[{'message_id': 'foo'},
329
{'message_id': 'bar'}]}):
330
response = client.get_messages('baz', limit=1)
331
self.assertEqual(1, len(response['messages']))
332
messages = response['messages']
333
response = client.get_messages(
334
'baz', limit=1, memo=response['next_memo'])
335
self.assertEqual(1, len(response['messages']))
336
messages.extend(response['messages'])
337
self.assertMessageIDs(['foo', 'bar'], messages)
339
def get_messages_member_order_test(self, key):
340
client = GrackleClient('localhost', 8439)
341
with ForkedFakeService.from_client(client,
342
{'baz': [{'message_id': 'foo', key: '2011-03-25'},
343
{'message_id': 'bar', key: '2011-03-24'}]}):
344
response = client.get_messages('baz')
345
self.assertIDOrder(['foo', 'bar'], response['messages'])
346
response = client.get_messages('baz', order=key)
347
self.assertIDOrder(['bar', 'foo'], response['messages'])
349
def test_get_messages_date_order(self):
350
self.get_messages_member_order_test('date')
352
def test_get_messages_author_order(self):
353
self.get_messages_member_order_test('author')
355
def test_get_messages_subject_order(self):
356
self.get_messages_member_order_test('subject')
358
def test_get_messages_thread_subject_order(self):
359
client = GrackleClient('localhost', 8439)
360
with ForkedFakeService.from_client(client, {'baz': [
361
{'message_id': 'bar', 'subject': 'y'},
362
{'message_id': 'qux', 'subject': 'z'},
363
{'message_id': 'foo', 'subject': 'x', 'in_reply_to': 'qux'},
365
response = client.get_messages('baz')
366
self.assertIDOrder(['bar', 'qux', 'foo'], response['messages'])
367
response = client.get_messages('baz', order='subject')
368
self.assertIDOrder(['foo', 'bar', 'qux'], response['messages'])
369
response = client.get_messages('baz', order='thread_subject')
370
self.assertIDOrder(['bar', 'qux', 'foo'], response['messages'])
372
def test_get_messages_thread_oldest_order(self):
373
client = GrackleClient('localhost', 8439)
374
with ForkedFakeService.from_client(client, {'baz': [
375
{'message_id': 'bar', 'date': 'x'},
376
{'message_id': 'qux', 'date': 'z'},
377
{'message_id': 'foo', 'date': 'y', 'in_reply_to': 'qux'},
379
response = client.get_messages('baz')
380
self.assertIDOrder(['bar', 'qux', 'foo'], response['messages'])
381
response = client.get_messages('baz', order='date')
382
self.assertIDOrder(['bar', 'foo', 'qux'], response['messages'])
383
response = client.get_messages('baz', order='thread_oldest')
384
self.assertIDOrder(['bar', 'qux', 'foo'], response['messages'])
386
def test_get_messages_thread_newest_order(self):
387
client = GrackleClient('localhost', 8439)
388
with ForkedFakeService.from_client(client, {'baz': [
389
{'message_id': 'bar', 'date': 'x'},
390
{'message_id': 'qux', 'date': 'w'},
391
{'message_id': 'foo', 'date': 'y', 'in_reply_to': 'bar'},
392
{'message_id': 'baz', 'date': 'z', 'in_reply_to': 'qux'},
394
response = client.get_messages('baz', order='date')
396
['qux', 'bar', 'foo', 'baz'], response['messages'])
397
response = client.get_messages('baz', order='thread_newest')
399
['bar', 'foo', 'qux', 'baz'], response['messages'])
401
def test_get_messages_unsupported_order(self):
402
client = GrackleClient('localhost', 8439)
403
with ForkedFakeService.from_client(client,
404
{'baz': [{'message_id': 'foo', 'date': '2011-03-25'},
405
{'message_id': 'bar', 'date': '2011-03-24'}]}):
406
with ExpectedException(UnsupportedOrder, ''):
407
client.get_messages('baz', order='nonsense')
409
def test_get_messages_headers_no_headers(self):
410
client = GrackleClient('localhost', 8440)
411
with ForkedFakeService.from_client(client,
413
{'message_id': 'foo'}
415
response = client.get_messages('baz', headers=[
416
'Subject', 'Date', 'X-Launchpad-Message-Rationale'])
417
first_message = response['messages'][0]
418
self.assertEqual('foo', first_message['message_id'])
419
self.assertEqual({}, first_message['headers'])
421
def test_get_messages_headers_exclude_headers(self):
422
client = GrackleClient('localhost', 8441)
423
with ForkedFakeService.from_client(client,
425
{'message_id': 'foo', 'headers': {'From': 'me'}}
427
response = client.get_messages('baz', headers=[
428
'Subject', 'Date', 'X-Launchpad-Message-Rationale'])
429
first_message = response['messages'][0]
430
self.assertEqual('foo', first_message['message_id'])
431
self.assertEqual({}, first_message['headers'])
433
def test_get_messages_headers_include_headers(self):
434
client = GrackleClient('localhost', 8442)
435
with ForkedFakeService.from_client(client,
437
{'message_id': 'foo', 'headers': {'From': 'me', 'To': 'you'}}
439
response = client.get_messages('baz', headers=[
441
first_message = response['messages'][0]
442
self.assertEqual('foo', first_message['message_id'])
443
self.assertEqual({'From': 'me', 'To': 'you'}, first_message['headers'])
445
def test_get_messages_max_body_length(self):
446
client = GrackleClient('localhost', 8443)
447
with ForkedFakeService.from_client(client,
449
{'message_id': 'foo', 'body': u'abcdefghi'}
451
response = client.get_messages('baz', max_body_length=3)
452
first_message = response['messages'][0]
453
self.assertEqual('abc', first_message['body'])
455
def test_include_hidden(self):
456
client = GrackleClient('localhost', 8444)
457
with ForkedFakeService.from_client(client,
459
{'message_id': 'foo', 'hidden': True},
460
{'message_id': 'bar', 'hidden': False}
462
response = client.get_messages('baz', include_hidden=True)
463
self.assertMessageIDs(['bar', 'foo'], response['messages'])
464
response = client.get_messages('baz', include_hidden=False)
465
self.assertMessageIDs(['bar'], response['messages'])
467
def test_display_type_unknown_value(self):
468
client = GrackleClient('localhost', 8445)
469
with ForkedFakeService.from_client(client,
471
{'message_id': 'foo', 'body': u'abcdefghi'}
473
with ExpectedException(UnsupportedDisplayType, ''):
474
client.get_messages('baz', display_type='unknown')
476
def test_display_type_headers_only(self):
477
client = GrackleClient('localhost', 8446)
478
with ForkedFakeService.from_client(client,
480
{'message_id': 'foo',
481
'headers': {'From': 'me', 'To': 'you'},
484
response = client.get_messages('baz', display_type='headers-only')
485
first_message = response['messages'][0]
486
self.assertEqual('foo', first_message['message_id'])
487
self.assertEqual({'From': 'me', 'To': 'you'}, first_message['headers'])
488
self.assertNotIn('body', first_message)
490
def test_display_type_text_only(self):
491
client = GrackleClient('localhost', 8446)
492
with ForkedFakeService.from_client(client,
495
'abcdefghi', message_id='foo',
496
headers={'From': 'me', 'To': 'you'},
497
attachment_type='text/x-diff')
499
response = client.get_messages('baz', display_type='text-only')
500
first_message = response['messages'][0]
501
self.assertEqual('foo', first_message['message_id'])
502
self.assertEqual('me', first_message['headers']['From'])
503
self.assertEqual('you', first_message['headers']['To'])
504
self.assertEqual('abcdefghi', first_message['body'])
506
def test_display_type_all(self):
507
client = GrackleClient('localhost', 8447)
508
with ForkedFakeService.from_client(client,
511
'abcdefghi', message_id='foo',
512
headers={'From': 'me', 'To': 'you'},
513
attachment_type='text/x-diff')
515
response = client.get_messages('baz', display_type='all')
516
first_message = response['messages'][0]
517
self.assertEqual('foo', first_message['message_id'])
518
self.assertEqual('me', first_message['headers']['From'])
519
self.assertEqual('you', first_message['headers']['To'])
521
'abcdefghi\n\nattactment data.', first_message['body'])
58
client.put_message('arch1', StringIO('This is not a message'))