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 (
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:
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."""
194
133
self.logger = logging.getLogger('http')
195
134
BaseHTTPRequestHandler.__init__(self, *args, **kwargs)
137
"""Create an archive or message on PUT."""
138
scheme, netloc, path, params, query_string, fragments = (
140
parts = path.split('/')
141
if parts[1] != 'archive':
142
# This is an unknonwn operation?
145
# This expected path is /archive/archive_id/message_id.
147
message = self.rfile.read(int(self.headers['content-length']))
148
self.server.store.put_message(parts[2], parts[3], message)
149
self.send_response(httplib.CREATED)
153
self.send_error(httplib.BAD_REQUEST)
197
155
def do_POST(self):
198
"""Create a message on POST."""
199
message = self.rfile.read(int(self.headers['content-length']))
200
if message == 'This is a message':
201
self.send_response(httplib.CREATED)
205
self.send_error(httplib.BAD_REQUEST)
156
"""Change a message on POST."""
157
scheme, netloc, path, params, query_string, fragments = (
159
parts = path.split('/')
160
if parts[1] != 'archive':
161
# This is an unknonwn operation?
164
# This expected path is /archive/archive_id/message_id.
166
# This expected path is /archive/archive_id/message_id.
167
response = self.server.store.hide_message(
168
parts[2], parts[3], query_string)
169
self.send_response(httplib.OK)
171
self.wfile.write(simplejson.dumps(response))
173
self.send_error(httplib.BAD_REQUEST)
207
175
def do_GET(self):
208
176
"""Retrieve a list of messages on GET."""
231
199
class TestPutMessage(TestCase):
233
201
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'))
202
client = GrackleClient('localhost', 8420)
203
message_archives = {'arch1': []}
204
with ForkedFakeService.from_client(client, message_archives):
205
client.put_message('arch1', 'id1', StringIO('This is a message'))
206
response = client.get_messages('arch1')
207
self.assertEqual(1, len(response['messages']))
208
message = response['messages'][0]
209
self.assertEqual('id1', message['message_id'])
211
def test_put_message_without_archive(self):
212
client = GrackleClient('localhost', 8421)
213
message_archives = {'arch1': []}
214
with ForkedFakeService.from_client(client, message_archives):
237
215
with ExpectedException(Exception, 'wtf'):
238
client.put_message('arch1', 'asdf',
239
StringIO('This is not a message'))
216
client.put_message('no-archive', 'id1', StringIO('message'))
242
219
class TestGetMessages(TestCase):
394
384
def test_get_messages_max_body_length(self):
395
385
client = GrackleClient('localhost', 8443)
396
with ForkedFake.from_client(client,
398
{'message_id': 'foo', 'body': u'abcdefghi'}
386
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
387
with ForkedFakeService.from_client(client, archive):
400
388
response = client.get_messages('baz', max_body_length=3)
401
389
first_message = response['messages'][0]
402
390
self.assertEqual('abc', first_message['body'])
404
392
def test_include_hidden(self):
405
393
client = GrackleClient('localhost', 8444)
406
with ForkedFake.from_client(client,
408
{'message_id': 'foo', 'hidden': True},
409
{'message_id': 'bar', 'hidden': False}
396
make_message('foo', hidden=True),
397
make_message('bar', hidden=False),
399
with ForkedFakeService.from_client(client, archive):
411
400
response = client.get_messages('baz', include_hidden=True)
412
401
self.assertMessageIDs(['bar', 'foo'], response['messages'])
413
402
response = client.get_messages('baz', include_hidden=False)
414
403
self.assertMessageIDs(['bar'], response['messages'])
405
def test_display_type_unknown_value(self):
406
client = GrackleClient('localhost', 8445)
407
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
408
with ForkedFakeService.from_client(client, archive):
409
with ExpectedException(UnsupportedDisplayType, ''):
410
client.get_messages('baz', display_type='unknown')
412
def test_display_type_headers_only(self):
413
client = GrackleClient('localhost', 8446)
416
make_message('foo', body=u'abcdefghi',
417
headers={'From': 'me', 'To': 'you'})]}
418
with ForkedFakeService.from_client(client, archive):
419
response = client.get_messages('baz', display_type='headers-only')
420
first_message = response['messages'][0]
421
self.assertEqual('foo', first_message['message_id'])
423
archive['baz'][0]['headers'], first_message['headers'])
424
self.assertNotIn('body', first_message)
426
def test_display_type_text_only(self):
427
client = GrackleClient('localhost', 8446)
432
headers={'From': 'me', 'To': 'you'},
433
attachment_type='text/x-diff')]}
434
with ForkedFakeService.from_client(client, archive):
435
response = client.get_messages('baz', display_type='text-only')
436
first_message = response['messages'][0]
437
self.assertEqual('foo', first_message['message_id'])
438
self.assertEqual('me', first_message['headers']['From'])
439
self.assertEqual('you', first_message['headers']['To'])
440
self.assertEqual(archive['baz'][0]['body'], first_message['body'])
442
def test_display_type_all(self):
443
client = GrackleClient('localhost', 8447)
448
headers={'From': 'me', 'To': 'you'},
449
attachment_type='text/x-diff')]}
450
with ForkedFakeService.from_client(client, archive):
451
response = client.get_messages('baz', display_type='all')
452
first_message = response['messages'][0]
453
self.assertEqual('foo', first_message['message_id'])
454
self.assertEqual('me', first_message['headers']['From'])
455
self.assertEqual('you', first_message['headers']['To'])
456
self.assertEqual(archive['baz'][0]['body'], first_message['body'])
458
def test_date_range(self):
459
client = GrackleClient('localhost', 8448)
463
'foo', 'abcdefghi', headers={'date': '2011-12-31'}),
465
'bar', 'abcdefghi', headers={'date': '2012-01-01'}),
467
'qux', 'abcdefghi', headers={'date': '2012-01-15'}),
469
'naf', 'abcdefghi', headers={'date': '2012-01-31'}),
471
'doh', 'abcdefghi', headers={'date': '2012-02-01'}),
473
with ForkedFakeService.from_client(client, archive):
474
response = client.get_messages(
475
'baz', date_range='2012-01-01..2012-01-31')
476
ids = sorted(m['message_id'] for m in response['messages'])
477
self.assertEqual(['bar', 'naf', 'qux'], ids)
479
def test_date_range_unparsabledaterange(self):
480
client = GrackleClient('localhost', 8449)
481
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
482
with ForkedFakeService.from_client(client, archive):
483
with ExpectedException(UnparsableDateRange, ''):
484
client.get_messages('baz', date_range='2012-01-01')
486
def test_date_range_unparsabledaterange_missing_part(self):
487
client = GrackleClient('localhost', 8450)
488
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
489
with ForkedFakeService.from_client(client, archive):
490
with ExpectedException(UnparsableDateRange, ''):
491
client.get_messages('baz', date_range='2012-01-01..')
493
def test_date_range_unparsabledaterange_extra_part(self):
494
client = GrackleClient('localhost', 8451)
495
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
496
with ForkedFakeService.from_client(client, archive):
497
with ExpectedException(UnparsableDateRange, ''):
498
client.get_messages('baz', date_range='2012-01..12-02..12-03')
501
class TestHideMessages(TestCase):
503
def test_hide_message_true(self):
504
client = GrackleClient('localhost', 8470)
507
make_message('foo', hidden=False),
509
with ForkedFakeService.from_client(client, archive):
510
response = client.hide_message('baz', 'foo', hidden=True)
511
self.assertEqual('foo', response['message_id'])
512
self.assertIs(True, response['hidden'])
514
def test_hide_message_false(self):
515
client = GrackleClient('localhost', 8470)
518
make_message('foo', hidden=True),
520
with ForkedFakeService.from_client(client, archive):
521
response = client.hide_message('baz', 'foo', hidden=False)
522
self.assertEqual('foo', response['message_id'])
523
self.assertIs(False, response['hidden'])