21
18
from grackle.client import (
23
UnsupportedDisplayType,
28
def make_message(message_id, body='body', headers=None, hidden=False):
31
headers['Message-Id'] = message_id
33
'message_id': message_id,
35
'thread_id': message_id,
36
'date': headers.get('date', '2005-01-01'),
37
'subject': headers.get('subject', 'subject'),
38
'author': headers.get('author', 'author'),
41
'replies': headers.get('in-reply-to', None),
47
def make_mime_message(message_id, body='body', headers=None, hidden=False,
48
attachment_type=None):
49
message = MIMEMultipart()
50
message.attach(MIMEText(body))
51
if attachment_type is not None:
52
attachment = Message()
53
attachment.set_payload('attactment data.')
54
attachment['Content-Type'] = attachment_type
55
attachment['Content-Disposition'] = 'attachment; filename="file.ext"'
56
message.attach(attachment)
57
return make_message(message_id, message.get_payload(), headers, hidden)
60
24
def threaded_messages(messages):
64
28
for message in messages:
65
if message.get('replies') is None:
29
if message.get('in_reply_to') is None:
66
30
threads[message['message_id']] = [message]
69
33
pending.append(message)
70
34
for message in pending:
71
threads[message['replies']].append(message)
35
threads[message['in_reply_to']].append(message)
72
36
return threads.values()
75
39
class GrackleStore:
76
"""A memory-backed message store."""
78
41
def __init__(self, messages):
80
42
self.messages = messages
83
def is_multipart(message):
84
return isinstance(message['body'], list)
86
44
def get_messages(self, archive_id, query_string):
87
"""Return matching messages.
89
:param archive_id: The archive to retrieve from.
90
:param query_string: Contains 'parameters', which is a JSON-format
91
string describing parameters.
93
45
query = parse_qs(query_string)
94
46
parameters = simplejson.loads(query['parameters'][0])
95
47
order = parameters.get('order')
96
48
messages = self.messages[archive_id]
49
if order is not None :
98
50
if order not in SUPPORTED_ORDERS:
99
51
raise UnsupportedOrder
100
52
elif order.startswith('thread_'):
126
78
(k, v) for k, v in message['headers'].iteritems()
127
79
if k in parameters['headers'])
128
80
message['headers'] = headers
129
if display_type == 'headers-only':
131
elif display_type == 'text-only' and self.is_multipart(message):
133
part.get_payload() for part in message['body']
134
if part.get_content_type() == 'text/plain']
135
message['body'] = '\n\n'.join(text_parts)
136
elif display_type == 'all' and self.is_multipart(message):
137
parts = [str(part.get_payload()) for part in message['body']]
138
message['body'] = '\n\n'.join(parts)
139
81
max_body = parameters.get('max_body_length')
140
if max_body is not None and display_type != 'headers-only':
82
if max_body is not None:
141
83
message['body'] = message['body'][:max_body]
142
84
new_messages.append(message)
143
85
messages = new_messages
188
122
self.messages = messages
189
123
self.read_end, self.write_end = os.pipe()
190
self.write_logs = write_logs
193
126
def from_client(client, messages=None):
194
"""Instantiate a ForkedFakeService from the client.
196
:param port: The client to provide service for.
197
:param messages: A dict of lists of dicts representing messages. The
198
outer dict represents the archive, the list represents the list of
199
messages for that archive.
201
return ForkedFakeService(client.port, messages)
127
return ForkedFake(client.port, messages)
203
129
def is_ready(self):
204
"""Tell the parent process that the server is ready for writes."""
205
130
os.write(self.write_end, 'asdf')
207
132
def __enter__(self):
210
Fork and start a server in the child. Return when the server is ready
214
135
self.start_server()
219
140
def start_server(self):
220
"""Start the HTTP server."""
221
141
service = HTTPServer(('', self.port), FakeGrackleRequestHandler)
222
142
service.store = GrackleStore(self.messages)
223
143
for archive_id, messages in service.store.messages.iteritems():
224
144
for message in messages:
225
145
message.setdefault('headers', {})
229
stream=sys.stderr, level=logging.INFO)
147
# logging.basicConfig(
148
# stream=sys.stderr, level=logging.INFO)
230
149
service.serve_forever()
232
151
def __exit__(self, exc_type, exc_val, traceback):
233
152
os.kill(self.pid, SIGKILL)
236
SUPPORTED_DISPLAY_TYPES = set(['all', 'text-only', 'headers-only'])
239
155
SUPPORTED_ORDERS = set(
240
156
['date', 'author', 'subject', 'thread_newest', 'thread_oldest',
241
157
'thread_subject'])
244
160
class FakeGrackleRequestHandler(BaseHTTPRequestHandler):
245
"""A request handler that forwards to server.store."""
247
162
def __init__(self, *args, **kwargs):
248
"""Constructor. Sets up logging."""
249
163
self.logger = logging.getLogger('http')
250
164
BaseHTTPRequestHandler.__init__(self, *args, **kwargs)
252
166
def do_POST(self):
253
"""Create a message on POST."""
254
167
message = self.rfile.read(int(self.headers['content-length']))
255
168
if message == 'This is a message':
256
169
self.send_response(httplib.CREATED)
272
184
self.end_headers()
273
185
self.wfile.write(simplejson.dumps(response))
274
186
except UnsupportedOrder:
276
httplib.BAD_REQUEST, UnsupportedOrder.__doc__)
278
except UnsupportedDisplayType:
280
httplib.BAD_REQUEST, UnsupportedDisplayType.__doc__)
187
self.send_response(httplib.BAD_REQUEST)
188
self.wfile.write('Unsupported order')
283
191
def log_message(self, format, *args):
284
"""Override log_message to use standard Python logging."""
285
192
message = "%s - - [%s] %s\n" % (
286
self.address_string(), self.log_date_time_string(), format % args)
193
self.address_string(), self.log_date_time_string(), format%args)
287
194
self.logger.info(message)
306
213
def assertMessageIDs(self, ids, messages):
307
214
self.assertIDOrder(
308
sorted(ids), sorted(messages, key=lambda m: m['message_id']))
215
sorted(ids), sorted(messages, key=lambda m:m['message_id']))
310
217
def test_get_messages(self):
311
218
client = GrackleClient('localhost', 8435)
313
'baz': [make_message('foo'), make_message('bar')]}
314
with ForkedFakeService.from_client(client, archive):
219
with ForkedFake.from_client(client,
221
[{'message_id': 'foo'},
222
{'message_id': 'bar'}]}):
315
223
response = client.get_messages('baz')
316
224
self.assertEqual(['bar', 'foo'], sorted(m['message_id'] for m in
317
225
response['messages']))
321
229
def test_get_messages_by_id(self):
322
230
client = GrackleClient('localhost', 8437)
324
'baz': [make_message('foo'), make_message('bar')]}
325
with ForkedFakeService.from_client(client, archive):
231
with ForkedFake.from_client(client,
233
[{'message_id': 'foo'},
234
{'message_id': 'bar'}]}):
326
235
response = client.get_messages('baz', message_ids=['foo'])
327
236
message, = response['messages']
328
237
self.assertEqual('foo', message['message_id'])
330
239
def test_get_messages_batching(self):
331
240
client = GrackleClient('localhost', 8438)
332
archive = {'baz': [make_message('foo'), make_message('bar')]}
333
with ForkedFakeService.from_client(client, archive):
241
with ForkedFake.from_client(client,
243
[{'message_id': 'foo'},
244
{'message_id': 'bar'}]}):
334
245
response = client.get_messages('baz', limit=1)
335
246
self.assertEqual(1, len(response['messages']))
336
247
messages = response['messages']
343
254
def get_messages_member_order_test(self, key):
344
255
client = GrackleClient('localhost', 8439)
347
make_message('foo', headers={key: '2011-03-25'}),
348
make_message('bar', headers={key: '2011-03-24'}),
350
with ForkedFakeService.from_client(client, archive):
256
with ForkedFake.from_client(client,
257
{'baz': [{'message_id': 'foo', key: '2011-03-25'},
258
{'message_id': 'bar', key: '2011-03-24'}]}):
351
259
response = client.get_messages('baz')
352
260
self.assertIDOrder(['foo', 'bar'], response['messages'])
353
261
response = client.get_messages('baz', order=key)
363
271
self.get_messages_member_order_test('subject')
365
273
def test_get_messages_thread_subject_order(self):
368
make_message('bar', headers={'subject': 'y'}),
369
make_message('qux', headers={'subject': 'z'}),
370
make_message('foo', headers={'subject': 'x',
371
'in-reply-to': 'qux'}),
373
274
client = GrackleClient('localhost', 8439)
374
with ForkedFakeService.from_client(client, archive):
275
with ForkedFake.from_client(client, {'baz': [
276
{'message_id': 'bar', 'subject': 'y'},
277
{'message_id': 'qux', 'subject': 'z'},
278
{'message_id': 'foo', 'subject': 'x', 'in_reply_to': 'qux'},
375
280
response = client.get_messages('baz')
376
281
self.assertIDOrder(['bar', 'qux', 'foo'], response['messages'])
377
282
response = client.get_messages('baz', order='subject')
382
287
def test_get_messages_thread_oldest_order(self):
383
288
client = GrackleClient('localhost', 8439)
386
make_message('bar', headers={'date': 'x'}),
387
make_message('qux', headers={'date': 'z'}),
388
make_message('foo', headers={'date': 'y',
389
'in-reply-to': 'qux'}),
391
with ForkedFakeService.from_client(client, archive):
289
with ForkedFake.from_client(client, {'baz': [
290
{'message_id': 'bar', 'date': 'x'},
291
{'message_id': 'qux', 'date': 'z'},
292
{'message_id': 'foo', 'date': 'y', 'in_reply_to': 'qux'},
392
294
response = client.get_messages('baz')
393
295
self.assertIDOrder(['bar', 'qux', 'foo'], response['messages'])
394
296
response = client.get_messages('baz', order='date')
399
301
def test_get_messages_thread_newest_order(self):
400
302
client = GrackleClient('localhost', 8439)
403
make_message('bar', headers={'date': 'x'}),
404
make_message('qux', headers={'date': 'w'}),
405
make_message('foo', headers={'date': 'y',
406
'in-reply-to': 'bar'}),
407
make_message('baz', headers={'date': 'z',
408
'in-reply-to': 'qux'}),
410
with ForkedFakeService.from_client(client, archive):
303
with ForkedFake.from_client(client, {'baz': [
304
{'message_id': 'bar', 'date': 'x'},
305
{'message_id': 'qux', 'date': 'w'},
306
{'message_id': 'foo', 'date': 'y', 'in_reply_to': 'bar'},
307
{'message_id': 'baz', 'date': 'z', 'in_reply_to': 'qux'},
411
309
response = client.get_messages('baz', order='date')
412
310
self.assertIDOrder(
413
311
['qux', 'bar', 'foo', 'baz'], response['messages'])
418
316
def test_get_messages_unsupported_order(self):
419
317
client = GrackleClient('localhost', 8439)
422
make_message('foo', headers={'date': '2011-03-25'}),
423
make_message('foo', headers={'date': '2011-03-24'}),
425
with ForkedFakeService.from_client(client, archive):
426
with ExpectedException(UnsupportedOrder, ''):
318
with ForkedFake.from_client(client,
319
{'baz': [{'message_id': 'foo', 'date': '2011-03-25'},
320
{'message_id': 'bar', 'date': '2011-03-24'}]}):
321
with ExpectedException(UnsupportedOrder):
427
322
client.get_messages('baz', order='nonsense')
429
324
def test_get_messages_headers_no_headers(self):
430
325
client = GrackleClient('localhost', 8440)
431
archive = {'baz': [make_message('foo')]}
432
with ForkedFakeService.from_client(client, archive):
326
with ForkedFake.from_client(client,
328
{'message_id': 'foo'}
433
330
response = client.get_messages('baz', headers=[
434
331
'Subject', 'Date', 'X-Launchpad-Message-Rationale'])
435
332
first_message = response['messages'][0]
462
360
def test_get_messages_max_body_length(self):
463
361
client = GrackleClient('localhost', 8443)
464
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
465
with ForkedFakeService.from_client(client, archive):
362
with ForkedFake.from_client(client,
364
{'message_id': 'foo', 'body': u'abcdefghi'}
466
366
response = client.get_messages('baz', max_body_length=3)
467
367
first_message = response['messages'][0]
468
368
self.assertEqual('abc', first_message['body'])
470
370
def test_include_hidden(self):
471
371
client = GrackleClient('localhost', 8444)
474
make_message('foo', hidden=True),
475
make_message('bar', hidden=False),
477
with ForkedFakeService.from_client(client, archive):
372
with ForkedFake.from_client(client,
374
{'message_id': 'foo', 'hidden': True},
375
{'message_id': 'bar', 'hidden': False}
478
377
response = client.get_messages('baz', include_hidden=True)
479
378
self.assertMessageIDs(['bar', 'foo'], response['messages'])
480
379
response = client.get_messages('baz', include_hidden=False)
481
380
self.assertMessageIDs(['bar'], response['messages'])
483
def test_display_type_unknown_value(self):
484
client = GrackleClient('localhost', 8445)
485
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
486
with ForkedFakeService.from_client(client, archive):
487
with ExpectedException(UnsupportedDisplayType, ''):
488
client.get_messages('baz', display_type='unknown')
490
def test_display_type_headers_only(self):
491
client = GrackleClient('localhost', 8446)
494
make_message('foo', body=u'abcdefghi',
495
headers={'From': 'me', 'To': 'you'})]}
496
with ForkedFakeService.from_client(client, archive):
497
response = client.get_messages('baz', display_type='headers-only')
498
first_message = response['messages'][0]
499
self.assertEqual('foo', first_message['message_id'])
501
{'From': 'me', 'Message-Id': 'foo', 'To': 'you'},
502
first_message['headers'])
503
self.assertNotIn('body', first_message)
505
def test_display_type_text_only(self):
506
client = GrackleClient('localhost', 8446)
511
headers={'From': 'me', 'To': 'you'},
512
attachment_type='text/x-diff')]}
513
with ForkedFakeService.from_client(client, archive):
514
response = client.get_messages('baz', display_type='text-only')
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'])
519
self.assertEqual('abcdefghi', first_message['body'])
521
def test_display_type_all(self):
522
client = GrackleClient('localhost', 8447)
527
headers={'From': 'me', 'To': 'you'},
528
attachment_type='text/x-diff')]}
529
with ForkedFakeService.from_client(client, archive):
530
response = client.get_messages('baz', display_type='all')
531
first_message = response['messages'][0]
532
self.assertEqual('foo', first_message['message_id'])
533
self.assertEqual('me', first_message['headers']['From'])
534
self.assertEqual('you', first_message['headers']['To'])
536
'abcdefghi\n\nattactment data.', first_message['body'])