15
12
from unittest import TestCase
16
13
from urlparse import urlparse
14
from urlparse import parse_qs
18
16
from testtools import ExpectedException
20
18
from grackle.client import (
23
UnsupportedDisplayType,
26
from grackle.store import (
32
def make_message(message_id, body='body', headers=None, hidden=False):
35
headers['Message-Id'] = message_id
37
'message_id': message_id,
39
'thread_id': message_id,
40
'date': headers.get('date', '2005-01-01'),
41
'subject': headers.get('subject', 'subject'),
42
'author': headers.get('author', 'author'),
45
'replies': headers.get('in-reply-to', None),
51
def make_mime_message(message_id, body='body', headers=None, hidden=False,
52
attachment_type=None):
55
parts = MIMEMultipart()
56
parts.attach(MIMEText(body))
57
if attachment_type is not None:
58
attachment = Message()
59
attachment.set_payload('attactment data.')
60
attachment['Content-Type'] = attachment_type
61
attachment['Content-Disposition'] = 'attachment; filename="file.ext"'
62
parts.attach(attachment)
64
message.set_payload(parts.as_string())
65
for key, value in headers.items():
67
return make_json_message(message_id, message.as_string())
70
class ForkedFakeService:
24
def threaded_messages(messages):
28
for message in messages:
29
if message.get('in_reply_to') is None:
30
threads[message['message_id']] = [message]
33
pending.append(message)
34
for message in pending:
35
threads[message['in_reply_to']].append(message)
36
return threads.values()
40
"""A memory-backed message store."""
42
def __init__(self, messages):
44
self.messages = messages
46
def get_messages(self, archive_id, query_string):
47
"""Return matching messages.
49
:param archive_id: The archive to retrieve from.
50
:param query_string: Contains 'parameters', which is a JSON-format
51
string describing parameters.
53
query = parse_qs(query_string)
54
parameters = simplejson.loads(query['parameters'][0])
55
order = parameters.get('order')
56
messages = self.messages[archive_id]
57
if order is not None :
58
if order not in SUPPORTED_ORDERS:
59
raise UnsupportedOrder
60
elif order.startswith('thread_'):
61
threaded = threaded_messages(messages)
63
if order == 'thread_subject':
64
threaded.sort(key=lambda t: t[0]['subject'])
65
if order == 'thread_oldest':
66
threaded.sort(key=lambda t: min(m['date'] for m in t))
67
if order == 'thread_newest':
68
threaded.sort(key=lambda t: max(m['date'] for m in t))
69
for thread in threaded:
70
messages.extend(thread)
72
messages.sort(key=lambda m: m[order])
74
for message in messages:
76
not parameters['include_hidden']
77
and message.get('hidden', False)):
80
if ('message_ids' in parameters and
81
message['message_id'] not in parameters['message_ids']):
83
message = dict(message)
84
if 'headers' in parameters:
86
(k, v) for k, v in message['headers'].iteritems()
87
if k in parameters['headers'])
88
message['headers'] = headers
89
max_body = parameters.get('max_body_length')
90
if max_body is not None:
91
message['body'] = message['body'][:max_body]
92
new_messages.append(message)
93
messages = new_messages
94
limit = parameters.get('limit', 100)
95
memo = parameters.get('memo')
96
message_id_indices = dict(
97
(m['message_id'], idx) for idx, m in enumerate(messages))
101
start = message_id_indices[memo.encode('rot13')]
103
previous_memo = messages[start - 1]['message_id'].encode('rot13')
106
end = min(start + limit, len(messages))
107
if end < len(messages):
108
next_memo = messages[end]['message_id'].encode('rot13')
111
messages = messages[start:end]
114
'messages': messages,
115
'next_memo': next_memo,
116
'previous_memo': previous_memo
71
123
"""A Grackle service fake, as a ContextManager."""
73
def __init__(self, port, message_archives=None, write_logs=False):
125
def __init__(self, port, messages=None, write_logs=False):
76
:param port: The tcp port to use.
77
:param message_archives: A dict of lists of dicts representing
78
archives of messages. The outer dict represents the archive,
79
the list represents the list of messages for that archive.
127
:param port: The tcp port to use
128
:param messages: A dict of lists of dicts representing messages. The
129
outer dict represents the archive, the list represents the list of
130
messages for that archive.
80
131
:param write_logs: If true, log messages will be written to stdout.
84
if message_archives is None:
85
self.message_archives = {}
87
self.message_archives = message_archives
138
self.messages = messages
88
139
self.read_end, self.write_end = os.pipe()
89
140
self.write_logs = write_logs
92
def from_client(client, message_archives=None):
93
"""Instantiate a ForkedFakeService from the client.
143
def from_client(client, messages=None):
144
"""Instantiate a ForkedFake from the client.
95
:param port: The client to provide service for.
96
:param message_archives: A dict of lists of dicts representing
97
archives of messages. The outer dict represents the archive,
98
the list represents the list of messages for that archive.
146
:param port: The client to provide service for.
147
:param messages: A dict of lists of dicts representing messages. The
148
outer dict represents the archive, the list represents the list of
149
messages for that archive.
100
return ForkedFakeService(client.port, message_archives)
151
return ForkedFake(client.port, messages)
102
153
def is_ready(self):
103
154
"""Tell the parent process that the server is ready for writes."""
168
218
self.send_response(httplib.OK)
169
219
self.end_headers()
170
220
self.wfile.write(simplejson.dumps(response))
171
except Exception, error:
173
httplib.BAD_REQUEST, error.__doc__)
221
except UnsupportedOrder:
222
self.send_response(httplib.BAD_REQUEST)
223
self.wfile.write('Unsupported order')
176
226
def log_message(self, format, *args):
177
227
"""Override log_message to use standard Python logging."""
178
228
message = "%s - - [%s] %s\n" % (
179
self.address_string(), self.log_date_time_string(), format % args)
229
self.address_string(), self.log_date_time_string(), format%args)
180
230
self.logger.info(message)
183
233
class TestPutMessage(TestCase):
185
235
def test_put_message(self):
186
client = GrackleClient('localhost', 8420)
187
message_archives = {'arch1': []}
188
with ForkedFakeService.from_client(client, message_archives):
189
client.put_message('arch1', 'id1', StringIO('This is a message'))
190
response = client.get_messages('arch1')
191
self.assertEqual(1, len(response['messages']))
192
message = response['messages'][0]
193
self.assertEqual('id1', message['message_id'])
195
def test_put_message_without_archive(self):
196
client = GrackleClient('localhost', 8421)
197
message_archives = {'arch1': []}
198
with ForkedFakeService.from_client(client, message_archives):
236
client = GrackleClient('localhost', 8436)
237
with ForkedFake.from_client(client):
238
client.put_message('arch1', 'asdf', StringIO('This is a message'))
199
239
with ExpectedException(Exception, 'wtf'):
200
client.put_message('no-archive', 'id1', StringIO('message'))
240
client.put_message('arch1', 'asdf',
241
StringIO('This is not a message'))
203
244
class TestGetMessages(TestCase):
223
265
def test_get_messages_by_id(self):
224
266
client = GrackleClient('localhost', 8437)
226
'baz': [make_message('foo'), make_message('bar')]}
227
with ForkedFakeService.from_client(client, archive):
267
with ForkedFake.from_client(client,
269
[{'message_id': 'foo'},
270
{'message_id': 'bar'}]}):
228
271
response = client.get_messages('baz', message_ids=['foo'])
229
272
message, = response['messages']
230
273
self.assertEqual('foo', message['message_id'])
232
275
def test_get_messages_batching(self):
233
276
client = GrackleClient('localhost', 8438)
234
archive = {'baz': [make_message('foo'), make_message('bar')]}
235
with ForkedFakeService.from_client(client, archive):
277
with ForkedFake.from_client(client,
279
[{'message_id': 'foo'},
280
{'message_id': 'bar'}]}):
236
281
response = client.get_messages('baz', limit=1)
237
282
self.assertEqual(1, len(response['messages']))
238
283
messages = response['messages']
301
337
def test_get_messages_thread_newest_order(self):
302
338
client = GrackleClient('localhost', 8439)
305
make_message('bar', headers={'date': 'x'}),
306
make_message('qux', headers={'date': 'w'}),
307
make_message('foo', headers={'date': 'y',
308
'in-reply-to': 'bar'}),
309
make_message('baz', headers={'date': 'z',
310
'in-reply-to': 'qux'}),
312
with ForkedFakeService.from_client(client, archive):
339
with ForkedFake.from_client(client, {'baz': [
340
{'message_id': 'bar', 'date': 'x'},
341
{'message_id': 'qux', 'date': 'w'},
342
{'message_id': 'foo', 'date': 'y', 'in_reply_to': 'bar'},
343
{'message_id': 'baz', 'date': 'z', 'in_reply_to': 'qux'},
313
345
response = client.get_messages('baz', order='date')
314
346
self.assertIDOrder(
315
347
['qux', 'bar', 'foo', 'baz'], response['messages'])
320
352
def test_get_messages_unsupported_order(self):
321
353
client = GrackleClient('localhost', 8439)
324
make_message('foo', headers={'date': '2011-03-25'}),
325
make_message('foo', headers={'date': '2011-03-24'}),
327
with ForkedFakeService.from_client(client, archive):
328
with ExpectedException(UnsupportedOrder, ''):
354
with ForkedFake.from_client(client,
355
{'baz': [{'message_id': 'foo', 'date': '2011-03-25'},
356
{'message_id': 'bar', 'date': '2011-03-24'}]}):
357
with ExpectedException(UnsupportedOrder):
329
358
client.get_messages('baz', order='nonsense')
331
360
def test_get_messages_headers_no_headers(self):
332
361
client = GrackleClient('localhost', 8440)
333
archive = {'baz': [make_message('foo')]}
334
with ForkedFakeService.from_client(client, archive):
362
with ForkedFake.from_client(client,
364
{'message_id': 'foo'}
335
366
response = client.get_messages('baz', headers=[
336
367
'Subject', 'Date', 'X-Launchpad-Message-Rationale'])
337
368
first_message = response['messages'][0]
364
396
def test_get_messages_max_body_length(self):
365
397
client = GrackleClient('localhost', 8443)
366
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
367
with ForkedFakeService.from_client(client, archive):
398
with ForkedFake.from_client(client,
400
{'message_id': 'foo', 'body': u'abcdefghi'}
368
402
response = client.get_messages('baz', max_body_length=3)
369
403
first_message = response['messages'][0]
370
404
self.assertEqual('abc', first_message['body'])
372
406
def test_include_hidden(self):
373
407
client = GrackleClient('localhost', 8444)
376
make_message('foo', hidden=True),
377
make_message('bar', hidden=False),
379
with ForkedFakeService.from_client(client, archive):
408
with ForkedFake.from_client(client,
410
{'message_id': 'foo', 'hidden': True},
411
{'message_id': 'bar', 'hidden': False}
380
413
response = client.get_messages('baz', include_hidden=True)
381
414
self.assertMessageIDs(['bar', 'foo'], response['messages'])
382
415
response = client.get_messages('baz', include_hidden=False)
383
416
self.assertMessageIDs(['bar'], response['messages'])
385
def test_display_type_unknown_value(self):
386
client = GrackleClient('localhost', 8445)
387
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
388
with ForkedFakeService.from_client(client, archive):
389
with ExpectedException(UnsupportedDisplayType, ''):
390
client.get_messages('baz', display_type='unknown')
392
def test_display_type_headers_only(self):
393
client = GrackleClient('localhost', 8446)
396
make_message('foo', body=u'abcdefghi',
397
headers={'From': 'me', 'To': 'you'})]}
398
with ForkedFakeService.from_client(client, archive):
399
response = client.get_messages('baz', display_type='headers-only')
400
first_message = response['messages'][0]
401
self.assertEqual('foo', first_message['message_id'])
403
{'From': 'me', 'Message-Id': 'foo', 'To': 'you'},
404
first_message['headers'])
405
self.assertNotIn('body', first_message)
407
def test_display_type_text_only(self):
408
client = GrackleClient('localhost', 8446)
413
headers={'From': 'me', 'To': 'you'},
414
attachment_type='text/x-diff')]}
415
with ForkedFakeService.from_client(client, archive):
416
response = client.get_messages('baz', display_type='text-only')
417
first_message = response['messages'][0]
418
self.assertEqual('foo', first_message['message_id'])
419
self.assertEqual('me', first_message['headers']['From'])
420
self.assertEqual('you', first_message['headers']['To'])
421
self.assertEqual(archive['baz'][0]['body'], first_message['body'])
423
def test_display_type_all(self):
424
client = GrackleClient('localhost', 8447)
429
headers={'From': 'me', 'To': 'you'},
430
attachment_type='text/x-diff')]}
431
with ForkedFakeService.from_client(client, archive):
432
response = client.get_messages('baz', display_type='all')
433
first_message = response['messages'][0]
434
self.assertEqual('foo', first_message['message_id'])
435
self.assertEqual('me', first_message['headers']['From'])
436
self.assertEqual('you', first_message['headers']['To'])
437
self.assertEqual(archive['baz'][0]['body'], first_message['body'])
439
def test_date_range(self):
440
client = GrackleClient('localhost', 8448)
444
'foo', 'abcdefghi', headers={'date': '2011-12-31'}),
446
'bar', 'abcdefghi', headers={'date': '2012-01-01'}),
448
'qux', 'abcdefghi', headers={'date': '2012-01-15'}),
450
'naf', 'abcdefghi', headers={'date': '2012-01-31'}),
452
'doh', 'abcdefghi', headers={'date': '2012-02-01'}),
454
with ForkedFakeService.from_client(client, archive):
455
response = client.get_messages(
456
'baz', date_range='2012-01-01..2012-01-31')
457
ids = sorted(m['message_id'] for m in response['messages'])
458
self.assertEqual(['bar', 'naf', 'qux'], ids)
460
def test_date_range_unparsabledaterange(self):
461
client = GrackleClient('localhost', 8449)
462
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
463
with ForkedFakeService.from_client(client, archive):
464
with ExpectedException(UnparsableDateRange, ''):
465
client.get_messages('baz', date_range='2012-01-01')
467
def test_date_range_unparsabledaterange_missing_part(self):
468
client = GrackleClient('localhost', 8450)
469
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
470
with ForkedFakeService.from_client(client, archive):
471
with ExpectedException(UnparsableDateRange, ''):
472
client.get_messages('baz', date_range='2012-01-01..')
474
def test_date_range_unparsabledaterange_extra_part(self):
475
client = GrackleClient('localhost', 8451)
476
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
477
with ForkedFakeService.from_client(client, archive):
478
with ExpectedException(UnparsableDateRange, ''):
479
client.get_messages('baz', date_range='2012-01..12-02..12-03')