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]
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
26
from grackle.store import (
32
def make_message(message_id, body='body', headers=None, hidden=False):
36
'Message-Id': message_id,
42
message_headers.update(headers.items())
44
message.set_payload(body)
45
for key, value in message_headers.items():
47
return make_json_message(message_id, message.as_string(), hidden)
50
def make_mime_message(message_id, body='body', headers=None, hidden=False,
51
attachment_type=None):
52
parts = MIMEMultipart()
53
parts.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
parts.attach(attachment)
60
return make_message(message_id, parts.as_string(), headers, hidden)
63
class ForkedFakeService:
123
64
"""A Grackle service fake, as a ContextManager."""
125
def __init__(self, port, messages=None, write_logs=False):
66
def __init__(self, port, message_archives=None, write_logs=False):
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.
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.
131
73
:param write_logs: If true, log messages will be written to stdout.
77
if message_archives is None:
78
self.message_archives = {}
138
self.messages = messages
80
self.message_archives = message_archives
139
81
self.read_end, self.write_end = os.pipe()
140
82
self.write_logs = write_logs
143
def from_client(client, messages=None):
144
"""Instantiate a ForkedFake from the client.
85
def from_client(client, message_archives=None):
86
"""Instantiate a ForkedFakeService from the client.
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.
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.
151
return ForkedFake(client.port, messages)
93
return ForkedFakeService(client.port, message_archives)
153
95
def is_ready(self):
154
96
"""Tell the parent process that the server is ready for writes."""
199
136
def do_POST(self):
200
137
"""Create a message on POST."""
201
message = self.rfile.read(int(self.headers['content-length']))
202
if message == 'This is a message':
203
self.send_response(httplib.CREATED)
138
scheme, netloc, path, params, query_string, fragments = (
140
parts = path.split('/')
141
if parts[1] != 'archive':
142
# This is an unknonwn operation?
144
if 'content-length' in self.headers:
145
operation = 'put_message'
207
self.send_error(httplib.BAD_REQUEST)
147
operation = 'hide_message'
148
if operation == 'put_message':
149
message = self.rfile.read(int(self.headers['content-length']))
151
# This expected path is /archive/archive_id/message_id.
152
self.server.store.put_message(parts[2], parts[3], message)
153
self.send_response(httplib.CREATED)
157
self.send_error(httplib.BAD_REQUEST)
158
elif operation == 'hide_message':
160
# This expected path is /archive/archive_id/message_id.
161
response = self.server.store.hide_message(
162
parts[2], parts[3], query_string)
163
self.send_response(httplib.OK)
165
self.wfile.write(simplejson.dumps(response))
167
self.send_error(httplib.BAD_REQUEST)
209
169
def do_GET(self):
210
170
"""Retrieve a list of messages on GET."""
218
178
self.send_response(httplib.OK)
219
179
self.end_headers()
220
180
self.wfile.write(simplejson.dumps(response))
221
except UnsupportedOrder:
222
self.send_response(httplib.BAD_REQUEST)
223
self.wfile.write('Unsupported order')
181
except Exception, error:
183
httplib.BAD_REQUEST, error.__doc__)
226
186
def log_message(self, format, *args):
227
187
"""Override log_message to use standard Python logging."""
228
188
message = "%s - - [%s] %s\n" % (
229
self.address_string(), self.log_date_time_string(), format%args)
189
self.address_string(), self.log_date_time_string(), format % args)
230
190
self.logger.info(message)
233
193
class TestPutMessage(TestCase):
235
195
def test_put_message(self):
236
client = GrackleClient('localhost', 8436)
237
with ForkedFake.from_client(client):
238
client.put_message('arch1', 'asdf', StringIO('This is a message'))
196
client = GrackleClient('localhost', 8420)
197
message_archives = {'arch1': []}
198
with ForkedFakeService.from_client(client, message_archives):
199
client.put_message('arch1', 'id1', StringIO('This is a message'))
200
response = client.get_messages('arch1')
201
self.assertEqual(1, len(response['messages']))
202
message = response['messages'][0]
203
self.assertEqual('id1', message['message_id'])
205
def test_put_message_without_archive(self):
206
client = GrackleClient('localhost', 8421)
207
message_archives = {'arch1': []}
208
with ForkedFakeService.from_client(client, message_archives):
239
209
with ExpectedException(Exception, 'wtf'):
240
client.put_message('arch1', 'asdf',
241
StringIO('This is not a message'))
210
client.put_message('no-archive', 'id1', StringIO('message'))
244
213
class TestGetMessages(TestCase):
396
378
def test_get_messages_max_body_length(self):
397
379
client = GrackleClient('localhost', 8443)
398
with ForkedFake.from_client(client,
400
{'message_id': 'foo', 'body': u'abcdefghi'}
380
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
381
with ForkedFakeService.from_client(client, archive):
402
382
response = client.get_messages('baz', max_body_length=3)
403
383
first_message = response['messages'][0]
404
384
self.assertEqual('abc', first_message['body'])
406
386
def test_include_hidden(self):
407
387
client = GrackleClient('localhost', 8444)
408
with ForkedFake.from_client(client,
410
{'message_id': 'foo', 'hidden': True},
411
{'message_id': 'bar', 'hidden': False}
390
make_message('foo', hidden=True),
391
make_message('bar', hidden=False),
393
with ForkedFakeService.from_client(client, archive):
413
394
response = client.get_messages('baz', include_hidden=True)
414
395
self.assertMessageIDs(['bar', 'foo'], response['messages'])
415
396
response = client.get_messages('baz', include_hidden=False)
416
397
self.assertMessageIDs(['bar'], response['messages'])
399
def test_display_type_unknown_value(self):
400
client = GrackleClient('localhost', 8445)
401
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
402
with ForkedFakeService.from_client(client, archive):
403
with ExpectedException(UnsupportedDisplayType, ''):
404
client.get_messages('baz', display_type='unknown')
406
def test_display_type_headers_only(self):
407
client = GrackleClient('localhost', 8446)
410
make_message('foo', body=u'abcdefghi',
411
headers={'From': 'me', 'To': 'you'})]}
412
with ForkedFakeService.from_client(client, archive):
413
response = client.get_messages('baz', display_type='headers-only')
414
first_message = response['messages'][0]
415
self.assertEqual('foo', first_message['message_id'])
417
archive['baz'][0]['headers'], first_message['headers'])
418
self.assertNotIn('body', first_message)
420
def test_display_type_text_only(self):
421
client = GrackleClient('localhost', 8446)
426
headers={'From': 'me', 'To': 'you'},
427
attachment_type='text/x-diff')]}
428
with ForkedFakeService.from_client(client, archive):
429
response = client.get_messages('baz', display_type='text-only')
430
first_message = response['messages'][0]
431
self.assertEqual('foo', first_message['message_id'])
432
self.assertEqual('me', first_message['headers']['From'])
433
self.assertEqual('you', first_message['headers']['To'])
434
self.assertEqual(archive['baz'][0]['body'], first_message['body'])
436
def test_display_type_all(self):
437
client = GrackleClient('localhost', 8447)
442
headers={'From': 'me', 'To': 'you'},
443
attachment_type='text/x-diff')]}
444
with ForkedFakeService.from_client(client, archive):
445
response = client.get_messages('baz', display_type='all')
446
first_message = response['messages'][0]
447
self.assertEqual('foo', first_message['message_id'])
448
self.assertEqual('me', first_message['headers']['From'])
449
self.assertEqual('you', first_message['headers']['To'])
450
self.assertEqual(archive['baz'][0]['body'], first_message['body'])
452
def test_date_range(self):
453
client = GrackleClient('localhost', 8448)
457
'foo', 'abcdefghi', headers={'date': '2011-12-31'}),
459
'bar', 'abcdefghi', headers={'date': '2012-01-01'}),
461
'qux', 'abcdefghi', headers={'date': '2012-01-15'}),
463
'naf', 'abcdefghi', headers={'date': '2012-01-31'}),
465
'doh', 'abcdefghi', headers={'date': '2012-02-01'}),
467
with ForkedFakeService.from_client(client, archive):
468
response = client.get_messages(
469
'baz', date_range='2012-01-01..2012-01-31')
470
ids = sorted(m['message_id'] for m in response['messages'])
471
self.assertEqual(['bar', 'naf', 'qux'], ids)
473
def test_date_range_unparsabledaterange(self):
474
client = GrackleClient('localhost', 8449)
475
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
476
with ForkedFakeService.from_client(client, archive):
477
with ExpectedException(UnparsableDateRange, ''):
478
client.get_messages('baz', date_range='2012-01-01')
480
def test_date_range_unparsabledaterange_missing_part(self):
481
client = GrackleClient('localhost', 8450)
482
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
483
with ForkedFakeService.from_client(client, archive):
484
with ExpectedException(UnparsableDateRange, ''):
485
client.get_messages('baz', date_range='2012-01-01..')
487
def test_date_range_unparsabledaterange_extra_part(self):
488
client = GrackleClient('localhost', 8451)
489
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
490
with ForkedFakeService.from_client(client, archive):
491
with ExpectedException(UnparsableDateRange, ''):
492
client.get_messages('baz', date_range='2012-01..12-02..12-03')
495
class TestHideMessages(TestCase):
497
def test_hide_message_true(self):
498
client = GrackleClient('localhost', 8470)
501
make_message('foo', hidden=False),
503
with ForkedFakeService.from_client(client, archive):
504
response = client.hide_message('baz', 'foo', hidden=True)
505
self.assertEqual('foo', response['message_id'])
506
self.assertIs(True, response['hidden'])
508
def test_hide_message_false(self):
509
client = GrackleClient('localhost', 8470)
512
make_message('foo', hidden=True),
514
with ForkedFakeService.from_client(client, archive):
515
response = client.hide_message('baz', 'foo', hidden=False)
516
self.assertEqual('foo', response['message_id'])
517
self.assertIs(False, response['hidden'])