12
15
from unittest import TestCase
13
16
from urlparse import urlparse
14
from urlparse import parse_qs
16
18
from testtools import ExpectedException
18
20
from grackle.client import (
23
UnsupportedDisplayType,
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]
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:
75
if (not parameters['include_hidden']
76
and message.get('hidden', False)):
79
if ('message_ids' in parameters
80
and message['message_id'] not in parameters['message_ids']):
82
message = dict(message)
83
if 'headers' in parameters:
85
(k, v) for k, v in message['headers'].iteritems()
86
if k in parameters['headers'])
87
message['headers'] = headers
88
max_body = parameters.get('max_body_length')
89
if max_body is not None:
90
message['body'] = message['body'][:max_body]
91
new_messages.append(message)
92
messages = new_messages
93
limit = parameters.get('limit', 100)
94
memo = parameters.get('memo')
95
message_id_indices = dict(
96
(m['message_id'], idx) for idx, m in enumerate(messages))
100
start = message_id_indices[memo.encode('rot13')]
102
previous_memo = messages[start - 1]['message_id'].encode('rot13')
105
end = min(start + limit, len(messages))
106
if end < len(messages):
107
next_memo = messages[end]['message_id'].encode('rot13')
110
messages = messages[start:end]
113
'messages': messages,
114
'next_memo': next_memo,
115
'previous_memo': previous_memo
26
from grackle.store import (
31
def make_message(message_id, body='body', headers=None, hidden=False):
34
headers['Message-Id'] = message_id
36
'message_id': message_id,
38
'thread_id': message_id,
39
'date': headers.get('date', '2005-01-01'),
40
'subject': headers.get('subject', 'subject'),
41
'author': headers.get('author', 'author'),
44
'replies': headers.get('in-reply-to', None),
50
def make_mime_message(message_id, body='body', headers=None, hidden=False,
51
attachment_type=None):
52
message = MIMEMultipart()
53
message.attach(MIMEText(body))
54
if attachment_type is not None:
55
attachment = Message()
56
attachment.set_payload('attactment data.')
57
attachment['Content-Type'] = attachment_type
58
attachment['Content-Disposition'] = 'attachment; filename="file.ext"'
59
message.attach(attachment)
60
return make_message(message_id, message.get_payload(), headers, hidden)
63
class ForkedFakeService:
121
64
"""A Grackle service fake, as a ContextManager."""
123
def __init__(self, port, messages=None, write_logs=False):
66
def __init__(self, port, message_archives=None, write_logs=False):
125
:param port: The tcp port to use
126
:param messages: A dict of lists of dicts representing messages. The
127
outer dict represents the archive, the list represents the list of
128
messages for that archive.
69
:param port: The tcp port to use.
70
:param message_archives: A dict of lists of dicts representing
71
archives of messages. The outer dict represents the archive,
72
the list represents the list of messages for that archive.
129
73
:param write_logs: If true, log messages will be written to stdout.
77
if message_archives is None:
78
self.message_archives = {}
136
self.messages = messages
80
self.message_archives = message_archives
137
81
self.read_end, self.write_end = os.pipe()
138
82
self.write_logs = write_logs
141
def from_client(client, messages=None):
142
"""Instantiate a ForkedFake from the client.
85
def from_client(client, message_archives=None):
86
"""Instantiate a ForkedFakeService from the client.
144
:param port: The client to provide service for.
145
:param messages: A dict of lists of dicts representing messages. The
146
outer dict represents the archive, the list represents the list of
147
messages for that archive.
88
:param port: The client to provide service for.
89
:param message_archives: A dict of lists of dicts representing
90
archives of messages. The outer dict represents the archive,
91
the list represents the list of messages for that archive.
149
return ForkedFake(client.port, messages)
93
return ForkedFakeService(client.port, message_archives)
151
95
def is_ready(self):
152
96
"""Tell the parent process that the server is ready for writes."""
231
176
class TestPutMessage(TestCase):
233
178
def test_put_message(self):
234
client = GrackleClient('localhost', 8436)
235
with ForkedFake.from_client(client):
236
client.put_message('arch1', 'asdf', StringIO('This is a message'))
179
client = GrackleClient('localhost', 8420)
180
message_archives = {'arch1': []}
181
with ForkedFakeService.from_client(client, message_archives):
182
client.put_message('arch1', 'id1', StringIO('This is a message'))
183
response = client.get_messages('arch1')
184
self.assertEqual(1, len(response['messages']))
185
message = response['messages'][0]
186
self.assertEqual('id1', message['message_id'])
188
def test_put_message_without_archive(self):
189
client = GrackleClient('localhost', 8421)
190
message_archives = {'arch1': []}
191
with ForkedFakeService.from_client(client, message_archives):
237
192
with ExpectedException(Exception, 'wtf'):
238
client.put_message('arch1', 'asdf',
239
StringIO('This is not a message'))
193
client.put_message('no-archive', 'id1', StringIO('message'))
242
196
class TestGetMessages(TestCase):
263
216
def test_get_messages_by_id(self):
264
217
client = GrackleClient('localhost', 8437)
265
with ForkedFake.from_client(client,
267
[{'message_id': 'foo'},
268
{'message_id': 'bar'}]}):
219
'baz': [make_message('foo'), make_message('bar')]}
220
with ForkedFakeService.from_client(client, archive):
269
221
response = client.get_messages('baz', message_ids=['foo'])
270
222
message, = response['messages']
271
223
self.assertEqual('foo', message['message_id'])
273
225
def test_get_messages_batching(self):
274
226
client = GrackleClient('localhost', 8438)
275
with ForkedFake.from_client(client,
277
[{'message_id': 'foo'},
278
{'message_id': 'bar'}]}):
227
archive = {'baz': [make_message('foo'), make_message('bar')]}
228
with ForkedFakeService.from_client(client, archive):
279
229
response = client.get_messages('baz', limit=1)
280
230
self.assertEqual(1, len(response['messages']))
281
231
messages = response['messages']
335
294
def test_get_messages_thread_newest_order(self):
336
295
client = GrackleClient('localhost', 8439)
337
with ForkedFake.from_client(client, {'baz': [
338
{'message_id': 'bar', 'date': 'x'},
339
{'message_id': 'qux', 'date': 'w'},
340
{'message_id': 'foo', 'date': 'y', 'in_reply_to': 'bar'},
341
{'message_id': 'baz', 'date': 'z', 'in_reply_to': 'qux'},
298
make_message('bar', headers={'date': 'x'}),
299
make_message('qux', headers={'date': 'w'}),
300
make_message('foo', headers={'date': 'y',
301
'in-reply-to': 'bar'}),
302
make_message('baz', headers={'date': 'z',
303
'in-reply-to': 'qux'}),
305
with ForkedFakeService.from_client(client, archive):
343
306
response = client.get_messages('baz', order='date')
344
307
self.assertIDOrder(
345
308
['qux', 'bar', 'foo', 'baz'], response['messages'])
350
313
def test_get_messages_unsupported_order(self):
351
314
client = GrackleClient('localhost', 8439)
352
with ForkedFake.from_client(client,
353
{'baz': [{'message_id': 'foo', 'date': '2011-03-25'},
354
{'message_id': 'bar', 'date': '2011-03-24'}]}):
317
make_message('foo', headers={'date': '2011-03-25'}),
318
make_message('foo', headers={'date': '2011-03-24'}),
320
with ForkedFakeService.from_client(client, archive):
355
321
with ExpectedException(UnsupportedOrder, ''):
356
322
client.get_messages('baz', order='nonsense')
358
324
def test_get_messages_headers_no_headers(self):
359
325
client = GrackleClient('localhost', 8440)
360
with ForkedFake.from_client(client,
362
{'message_id': 'foo'}
326
archive = {'baz': [make_message('foo')]}
327
with ForkedFakeService.from_client(client, archive):
364
328
response = client.get_messages('baz', headers=[
365
329
'Subject', 'Date', 'X-Launchpad-Message-Rationale'])
366
330
first_message = response['messages'][0]
394
357
def test_get_messages_max_body_length(self):
395
358
client = GrackleClient('localhost', 8443)
396
with ForkedFake.from_client(client,
398
{'message_id': 'foo', 'body': u'abcdefghi'}
359
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
360
with ForkedFakeService.from_client(client, archive):
400
361
response = client.get_messages('baz', max_body_length=3)
401
362
first_message = response['messages'][0]
402
363
self.assertEqual('abc', first_message['body'])
404
365
def test_include_hidden(self):
405
366
client = GrackleClient('localhost', 8444)
406
with ForkedFake.from_client(client,
408
{'message_id': 'foo', 'hidden': True},
409
{'message_id': 'bar', 'hidden': False}
369
make_message('foo', hidden=True),
370
make_message('bar', hidden=False),
372
with ForkedFakeService.from_client(client, archive):
411
373
response = client.get_messages('baz', include_hidden=True)
412
374
self.assertMessageIDs(['bar', 'foo'], response['messages'])
413
375
response = client.get_messages('baz', include_hidden=False)
414
376
self.assertMessageIDs(['bar'], response['messages'])
378
def test_display_type_unknown_value(self):
379
client = GrackleClient('localhost', 8445)
380
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
381
with ForkedFakeService.from_client(client, archive):
382
with ExpectedException(UnsupportedDisplayType, ''):
383
client.get_messages('baz', display_type='unknown')
385
def test_display_type_headers_only(self):
386
client = GrackleClient('localhost', 8446)
389
make_message('foo', body=u'abcdefghi',
390
headers={'From': 'me', 'To': 'you'})]}
391
with ForkedFakeService.from_client(client, archive):
392
response = client.get_messages('baz', display_type='headers-only')
393
first_message = response['messages'][0]
394
self.assertEqual('foo', first_message['message_id'])
396
{'From': 'me', 'Message-Id': 'foo', 'To': 'you'},
397
first_message['headers'])
398
self.assertNotIn('body', first_message)
400
def test_display_type_text_only(self):
401
client = GrackleClient('localhost', 8446)
406
headers={'From': 'me', 'To': 'you'},
407
attachment_type='text/x-diff')]}
408
with ForkedFakeService.from_client(client, archive):
409
response = client.get_messages('baz', display_type='text-only')
410
first_message = response['messages'][0]
411
self.assertEqual('foo', first_message['message_id'])
412
self.assertEqual('me', first_message['headers']['From'])
413
self.assertEqual('you', first_message['headers']['To'])
414
self.assertEqual('abcdefghi', first_message['body'])
416
def test_display_type_all(self):
417
client = GrackleClient('localhost', 8447)
422
headers={'From': 'me', 'To': 'you'},
423
attachment_type='text/x-diff')]}
424
with ForkedFakeService.from_client(client, archive):
425
response = client.get_messages('baz', display_type='all')
426
first_message = response['messages'][0]
427
self.assertEqual('foo', first_message['message_id'])
428
self.assertEqual('me', first_message['headers']['From'])
429
self.assertEqual('you', first_message['headers']['To'])
431
'abcdefghi\n\nattactment data.', first_message['body'])
433
def test_date_range(self):
434
client = GrackleClient('localhost', 8448)
438
'foo', 'abcdefghi', headers={'date': '2011-12-31'}),
440
'bar', 'abcdefghi', headers={'date': '2012-01-01'}),
442
'qux', 'abcdefghi', headers={'date': '2012-01-15'}),
444
'naf', 'abcdefghi', headers={'date': '2012-01-31'}),
446
'doh', 'abcdefghi', headers={'date': '2012-02-01'}),
448
with ForkedFakeService.from_client(client, archive):
449
response = client.get_messages(
450
'baz', date_range='2012-01-01..2012-01-31')
451
ids = sorted(m['message_id'] for m in response['messages'])
452
self.assertEqual(['bar', 'naf', 'qux'], ids)
454
def test_date_range_unparsabledaterange(self):
455
client = GrackleClient('localhost', 8449)
456
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
457
with ForkedFakeService.from_client(client, archive):
458
with ExpectedException(UnparsableDateRange, ''):
459
client.get_messages('baz', date_range='2012-01-01')
461
def test_date_range_unparsabledaterange_missing_part(self):
462
client = GrackleClient('localhost', 8450)
463
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
464
with ForkedFakeService.from_client(client, archive):
465
with ExpectedException(UnparsableDateRange, ''):
466
client.get_messages('baz', date_range='2012-01-01..')
468
def test_date_range_unparsabledaterange_extra_part(self):
469
client = GrackleClient('localhost', 8451)
470
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
471
with ForkedFakeService.from_client(client, archive):
472
with ExpectedException(UnparsableDateRange, ''):
473
client.get_messages('baz', date_range='2012-01..12-02..12-03')