21
16
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
22
def threaded_messages(messages):
65
26
for message in messages:
66
if message.get('replies') is None:
27
if message.get('in_reply_to') is None:
67
28
threads[message['message_id']] = [message]
70
31
pending.append(message)
71
32
for message in pending:
72
threads[message['replies']].append(message)
33
threads[message['in_reply_to']].append(message)
73
34
return threads.values()
76
37
class GrackleStore:
77
"""A memory-backed message store."""
79
39
def __init__(self, messages):
81
40
self.messages = messages
84
def is_multipart(message):
85
return isinstance(message['body'], list)
87
42
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
43
query = parse_qs(query_string)
95
44
parameters = simplejson.loads(query['parameters'][0])
96
45
order = parameters.get('order')
97
46
messages = self.messages[archive_id]
47
if order is not None :
99
48
if order not in SUPPORTED_ORDERS:
100
49
raise UnsupportedOrder
101
50
elif order.startswith('thread_'):
111
60
messages.extend(thread)
113
62
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
63
messages = [m for m in messages
64
if 'message_ids' not in parameters or
65
m['message_id'] in parameters['message_ids']]
154
66
limit = parameters.get('limit', 100)
155
67
memo = parameters.get('memo')
156
68
message_id_indices = dict(
171
83
messages = messages[start:end]
85
for message in messages:
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 max_body is not None:
94
message['body'] = message['body'][:max_body]
95
new_messages.append(message)
174
'messages': messages,
97
'messages': new_messages,
175
98
'next_memo': next_memo,
176
99
'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.
107
def __init__(self, port, messages=None):
198
self.messages = messages
110
self.messages = messages
199
111
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)
213
113
def is_ready(self):
214
"""Tell the parent process that the server is ready for writes."""
215
114
os.write(self.write_end, 'asdf')
217
116
def __enter__(self):
220
Fork and start a server in the child. Return when the server is ready
224
119
self.start_server()
229
124
def start_server(self):
230
"""Start the HTTP server."""
231
125
service = HTTPServer(('', self.port), FakeGrackleRequestHandler)
232
126
service.store = GrackleStore(self.messages)
233
127
for archive_id, messages in service.store.messages.iteritems():
234
128
for message in messages:
235
129
message.setdefault('headers', {})
239
stream=sys.stderr, level=logging.INFO)
240
131
service.serve_forever()
242
133
def __exit__(self, exc_type, exc_val, traceback):
243
134
os.kill(self.pid, SIGKILL)
246
SUPPORTED_DISPLAY_TYPES = set(['all', 'text-only', 'headers-only'])
249
137
SUPPORTED_ORDERS = set(
250
138
['date', 'author', 'subject', 'thread_newest', 'thread_oldest',
251
139
'thread_subject'])
254
142
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
144
def do_POST(self):
263
"""Create a message on POST."""
264
145
message = self.rfile.read(int(self.headers['content-length']))
265
146
if message == 'This is a message':
266
147
self.send_response(httplib.CREATED)
282
162
self.end_headers()
283
163
self.wfile.write(simplejson.dumps(response))
284
164
except UnsupportedOrder:
286
httplib.BAD_REQUEST, UnsupportedOrder.__doc__)
288
except UnsupportedDisplayType:
290
httplib.BAD_REQUEST, UnsupportedDisplayType.__doc__)
293
def log_message(self, format, *args):
294
"""Override log_message to use standard Python logging."""
295
message = "%s - - [%s] %s\n" % (
296
self.address_string(), self.log_date_time_string(), format % args)
297
self.logger.info(message)
165
self.send_response(httplib.BAD_REQUEST)
166
self.wfile.write('Unsupported order')
170
def fake_grackle_service(client, messages=None):
173
return ForkedFake(client.port, messages)
300
176
class TestPutMessage(TestCase):
302
178
def test_put_message(self):
303
179
client = GrackleClient('localhost', 8436)
304
with ForkedFakeService.from_client(client):
180
with fake_grackle_service(client):
305
181
client.put_message('arch1', 'asdf', StringIO('This is a message'))
306
182
with ExpectedException(Exception, 'wtf'):
307
183
client.put_message('arch1', 'asdf',
331
208
def test_get_messages_by_id(self):
332
209
client = GrackleClient('localhost', 8437)
334
'baz': [make_message('foo'), make_message('bar')]}
335
with ForkedFakeService.from_client(client, archive):
210
with fake_grackle_service(client,
212
[{'message_id': 'foo'},
213
{'message_id': 'bar'}]}):
336
214
response = client.get_messages('baz', message_ids=['foo'])
337
215
message, = response['messages']
338
216
self.assertEqual('foo', message['message_id'])
340
218
def test_get_messages_batching(self):
341
219
client = GrackleClient('localhost', 8438)
342
archive = {'baz': [make_message('foo'), make_message('bar')]}
343
with ForkedFakeService.from_client(client, archive):
220
with fake_grackle_service(client,
222
[{'message_id': 'foo'},
223
{'message_id': 'bar'}]}):
344
224
response = client.get_messages('baz', limit=1)
345
225
self.assertEqual(1, len(response['messages']))
346
226
messages = response['messages']
373
250
self.get_messages_member_order_test('subject')
375
252
def test_get_messages_thread_subject_order(self):
378
make_message('bar', headers={'subject': 'y'}),
379
make_message('qux', headers={'subject': 'z'}),
380
make_message('foo', headers={'subject': 'x',
381
'in-reply-to': 'qux'}),
383
253
client = GrackleClient('localhost', 8439)
384
with ForkedFakeService.from_client(client, archive):
254
with fake_grackle_service(client, {'baz': [
255
{'message_id': 'bar', 'subject': 'y'},
256
{'message_id': 'qux', 'subject': 'z'},
257
{'message_id': 'foo', 'subject': 'x', 'in_reply_to': 'qux'},
385
259
response = client.get_messages('baz')
386
260
self.assertIDOrder(['bar', 'qux', 'foo'], response['messages'])
387
261
response = client.get_messages('baz', order='subject')
392
266
def test_get_messages_thread_oldest_order(self):
393
267
client = GrackleClient('localhost', 8439)
396
make_message('bar', headers={'date': 'x'}),
397
make_message('qux', headers={'date': 'z'}),
398
make_message('foo', headers={'date': 'y',
399
'in-reply-to': 'qux'}),
401
with ForkedFakeService.from_client(client, archive):
268
with fake_grackle_service(client, {'baz': [
269
{'message_id': 'bar', 'date': 'x'},
270
{'message_id': 'qux', 'date': 'z'},
271
{'message_id': 'foo', 'date': 'y', 'in_reply_to': 'qux'},
402
273
response = client.get_messages('baz')
403
274
self.assertIDOrder(['bar', 'qux', 'foo'], response['messages'])
404
275
response = client.get_messages('baz', order='date')
409
280
def test_get_messages_thread_newest_order(self):
410
281
client = GrackleClient('localhost', 8439)
413
make_message('bar', headers={'date': 'x'}),
414
make_message('qux', headers={'date': 'w'}),
415
make_message('foo', headers={'date': 'y',
416
'in-reply-to': 'bar'}),
417
make_message('baz', headers={'date': 'z',
418
'in-reply-to': 'qux'}),
420
with ForkedFakeService.from_client(client, archive):
282
with fake_grackle_service(client, {'baz': [
283
{'message_id': 'bar', 'date': 'x'},
284
{'message_id': 'qux', 'date': 'w'},
285
{'message_id': 'foo', 'date': 'y', 'in_reply_to': 'bar'},
286
{'message_id': 'baz', 'date': 'z', 'in_reply_to': 'qux'},
421
288
response = client.get_messages('baz', order='date')
422
289
self.assertIDOrder(
423
290
['qux', 'bar', 'foo', 'baz'], response['messages'])
428
295
def test_get_messages_unsupported_order(self):
429
296
client = GrackleClient('localhost', 8439)
432
make_message('foo', headers={'date': '2011-03-25'}),
433
make_message('foo', headers={'date': '2011-03-24'}),
435
with ForkedFakeService.from_client(client, archive):
436
with ExpectedException(UnsupportedOrder, ''):
297
with fake_grackle_service(client,
298
{'baz': [{'message_id': 'foo', 'date': '2011-03-25'},
299
{'message_id': 'bar', 'date': '2011-03-24'}]}):
300
with ExpectedException(UnsupportedOrder):
437
301
client.get_messages('baz', order='nonsense')
439
303
def test_get_messages_headers_no_headers(self):
440
304
client = GrackleClient('localhost', 8440)
441
archive = {'baz': [make_message('foo')]}
442
with ForkedFakeService.from_client(client, archive):
305
with fake_grackle_service(client,
307
{'message_id': 'foo'}
443
309
response = client.get_messages('baz', headers=[
444
310
'Subject', 'Date', 'X-Launchpad-Message-Rationale'])
445
311
first_message = response['messages'][0]
470
337
self.assertEqual({'From': 'me', 'To': 'you'}, first_message['headers'])
472
339
def test_get_messages_max_body_length(self):
473
client = GrackleClient('localhost', 8443)
474
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
475
with ForkedFakeService.from_client(client, archive):
340
client = GrackleClient('localhost', 8440)
341
with fake_grackle_service(client,
343
{'message_id': 'foo', 'body': u'abcdefghi'}
476
345
response = client.get_messages('baz', max_body_length=3)
477
346
first_message = response['messages'][0]
478
347
self.assertEqual('abc', first_message['body'])
480
def test_include_hidden(self):
481
client = GrackleClient('localhost', 8444)
484
make_message('foo', hidden=True),
485
make_message('bar', hidden=False),
487
with ForkedFakeService.from_client(client, archive):
488
response = client.get_messages('baz', include_hidden=True)
489
self.assertMessageIDs(['bar', 'foo'], response['messages'])
490
response = client.get_messages('baz', include_hidden=False)
491
self.assertMessageIDs(['bar'], response['messages'])
493
def test_display_type_unknown_value(self):
494
client = GrackleClient('localhost', 8445)
495
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
496
with ForkedFakeService.from_client(client, archive):
497
with ExpectedException(UnsupportedDisplayType, ''):
498
client.get_messages('baz', display_type='unknown')
500
def test_display_type_headers_only(self):
501
client = GrackleClient('localhost', 8446)
504
make_message('foo', body=u'abcdefghi',
505
headers={'From': 'me', 'To': 'you'})]}
506
with ForkedFakeService.from_client(client, archive):
507
response = client.get_messages('baz', display_type='headers-only')
508
first_message = response['messages'][0]
509
self.assertEqual('foo', first_message['message_id'])
511
{'From': 'me', 'Message-Id': 'foo', 'To': 'you'},
512
first_message['headers'])
513
self.assertNotIn('body', first_message)
515
def test_display_type_text_only(self):
516
client = GrackleClient('localhost', 8446)
521
headers={'From': 'me', 'To': 'you'},
522
attachment_type='text/x-diff')]}
523
with ForkedFakeService.from_client(client, archive):
524
response = client.get_messages('baz', display_type='text-only')
525
first_message = response['messages'][0]
526
self.assertEqual('foo', first_message['message_id'])
527
self.assertEqual('me', first_message['headers']['From'])
528
self.assertEqual('you', first_message['headers']['To'])
529
self.assertEqual('abcdefghi', first_message['body'])
531
def test_display_type_all(self):
532
client = GrackleClient('localhost', 8447)
537
headers={'From': 'me', 'To': 'you'},
538
attachment_type='text/x-diff')]}
539
with ForkedFakeService.from_client(client, archive):
540
response = client.get_messages('baz', display_type='all')
541
first_message = response['messages'][0]
542
self.assertEqual('foo', first_message['message_id'])
543
self.assertEqual('me', first_message['headers']['From'])
544
self.assertEqual('you', first_message['headers']['To'])
546
'abcdefghi\n\nattactment data.', first_message['body'])
548
def test_date_range(self):
549
client = GrackleClient('localhost', 8448)
553
'foo', 'abcdefghi', headers={'date': '2011-12-31'}),
555
'bar', 'abcdefghi', headers={'date': '2012-01-01'}),
557
'qux', 'abcdefghi', headers={'date': '2012-01-15'}),
559
'naf', 'abcdefghi', headers={'date': '2012-01-31'}),
561
'doh', 'abcdefghi', headers={'date': '2012-02-01'}),
563
with ForkedFakeService.from_client(client, archive):
564
response = client.get_messages(
565
'baz', date_range='2012-01-01..2012-01-31')
566
ids = sorted(m['message_id'] for m in response['messages'])
567
self.assertEqual(['bar', 'naf', 'qux'], ids)