15
15
from unittest import TestCase
16
16
from urlparse import urlparse
17
from urlparse import parse_qs
18
19
from testtools import ExpectedException
20
21
from grackle.client import (
23
23
UnsupportedDisplayType,
26
from grackle.store import (
32
28
def make_message(message_id, body='body', headers=None, hidden=False):
33
29
if headers is None:
36
'Message-Id': message_id,
31
headers['Message-Id'] = message_id
33
'message_id': message_id,
35
'thread_id': message_id,
36
'date': headers.get('date', '2005-01-01'),
37
'subject': headers.get('subject', 'subject'),
38
'author': headers.get('author', 'author'),
41
'replies': headers.get('in-reply-to', None),
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
47
def make_mime_message(message_id, body='body', headers=None, hidden=False,
51
48
attachment_type=None):
52
parts = MIMEMultipart()
53
parts.attach(MIMEText(body))
49
message = MIMEMultipart()
50
message.attach(MIMEText(body))
54
51
if attachment_type is not None:
55
52
attachment = Message()
56
53
attachment.set_payload('attactment data.')
57
54
attachment['Content-Type'] = attachment_type
58
55
attachment['Content-Disposition'] = 'attachment; filename="file.ext"'
59
parts.attach(attachment)
60
return make_message(message_id, parts.as_string(), headers, hidden)
56
message.attach(attachment)
57
return make_message(message_id, message.get_payload(), headers, hidden)
60
def threaded_messages(messages):
64
for message in messages:
65
if message.get('replies') is None:
66
threads[message['message_id']] = [message]
69
pending.append(message)
70
for message in pending:
71
threads[message['replies']].append(message)
72
return threads.values()
76
"""A memory-backed message store."""
78
def __init__(self, messages):
80
self.messages = messages
83
def is_multipart(message):
84
return isinstance(message['body'], list)
86
def get_messages(self, archive_id, query_string):
87
"""Return matching messages.
89
:param archive_id: The archive to retrieve from.
90
:param query_string: Contains 'parameters', which is a JSON-format
91
string describing parameters.
93
query = parse_qs(query_string)
94
parameters = simplejson.loads(query['parameters'][0])
95
order = parameters.get('order')
96
messages = self.messages[archive_id]
98
if order not in SUPPORTED_ORDERS:
99
raise UnsupportedOrder
100
elif order.startswith('thread_'):
101
threaded = threaded_messages(messages)
103
if order == 'thread_subject':
104
threaded.sort(key=lambda t: t[0]['subject'])
105
if order == 'thread_oldest':
106
threaded.sort(key=lambda t: min(m['date'] for m in t))
107
if order == 'thread_newest':
108
threaded.sort(key=lambda t: max(m['date'] for m in t))
109
for thread in threaded:
110
messages.extend(thread)
112
messages.sort(key=lambda m: m[order])
113
display_type = parameters.get('display_type', 'all')
114
if display_type not in SUPPORTED_DISPLAY_TYPES:
115
raise UnsupportedDisplayType
117
for message in messages:
118
if (not parameters['include_hidden'] and message['hidden']):
120
if ('message_ids' in parameters
121
and message['message_id'] not in parameters['message_ids']):
123
message = dict(message)
124
if 'headers' in parameters:
126
(k, v) for k, v in message['headers'].iteritems()
127
if k in parameters['headers'])
128
message['headers'] = headers
129
if display_type == 'headers-only':
131
elif display_type == 'text-only' and self.is_multipart(message):
133
part.get_payload() for part in message['body']
134
if part.get_content_type() == 'text/plain']
135
message['body'] = '\n\n'.join(text_parts)
136
elif display_type == 'all' and self.is_multipart(message):
137
parts = [str(part.get_payload()) for part in message['body']]
138
message['body'] = '\n\n'.join(parts)
139
max_body = parameters.get('max_body_length')
140
if max_body is not None and display_type != 'headers-only':
141
message['body'] = message['body'][:max_body]
142
new_messages.append(message)
143
messages = new_messages
144
limit = parameters.get('limit', 100)
145
memo = parameters.get('memo')
146
message_id_indices = dict(
147
(m['message_id'], idx) for idx, m in enumerate(messages))
151
start = message_id_indices[memo.encode('rot13')]
153
previous_memo = messages[start - 1]['message_id'].encode('rot13')
156
end = min(start + limit, len(messages))
157
if end < len(messages):
158
next_memo = messages[end]['message_id'].encode('rot13')
161
messages = messages[start:end]
164
'messages': messages,
165
'next_memo': next_memo,
166
'previous_memo': previous_memo
63
171
class ForkedFakeService:
64
172
"""A Grackle service fake, as a ContextManager."""
66
def __init__(self, port, message_archives=None, write_logs=False):
174
def __init__(self, port, messages=None, write_logs=False):
69
177
: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.
178
:param messages: A dict of lists of dicts representing messages. The
179
outer dict represents the archive, the list represents the list of
180
messages for that archive.
73
181
:param write_logs: If true, log messages will be written to stdout.
77
if message_archives is None:
78
self.message_archives = {}
80
self.message_archives = message_archives
188
self.messages = messages
81
189
self.read_end, self.write_end = os.pipe()
82
190
self.write_logs = write_logs
85
def from_client(client, message_archives=None):
193
def from_client(client, messages=None):
86
194
"""Instantiate a ForkedFakeService from the client.
88
196
: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.
197
:param messages: A dict of lists of dicts representing messages. The
198
outer dict represents the archive, the list represents the list of
199
messages for that archive.
93
return ForkedFakeService(client.port, message_archives)
201
return ForkedFakeService(client.port, messages)
95
203
def is_ready(self):
96
204
"""Tell the parent process that the server is ready for writes."""
136
252
def do_POST(self):
137
253
"""Create a message on POST."""
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'
254
message = self.rfile.read(int(self.headers['content-length']))
255
if message == 'This is a message':
256
self.send_response(httplib.CREATED)
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)
260
self.send_error(httplib.BAD_REQUEST)
169
262
def do_GET(self):
170
263
"""Retrieve a list of messages on GET."""
193
290
class TestPutMessage(TestCase):
195
292
def test_put_message(self):
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):
293
client = GrackleClient('localhost', 8436)
294
with ForkedFakeService.from_client(client):
295
client.put_message('arch1', 'asdf', StringIO('This is a message'))
209
296
with ExpectedException(Exception, 'wtf'):
210
client.put_message('no-archive', 'id1', StringIO('message'))
297
client.put_message('arch1', 'asdf',
298
StringIO('This is not a message'))
213
301
class TestGetMessages(TestCase):
447
532
self.assertEqual('foo', first_message['message_id'])
448
533
self.assertEqual('me', first_message['headers']['From'])
449
534
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'])
536
'abcdefghi\n\nattactment data.', first_message['body'])