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
from grackle.client import GrackleClient
21
from grackle.error import (
21
from grackle.client import (
24
23
UnsupportedDisplayType,
27
from grackle.service import ForkedFakeService
28
from grackle.store import (
34
28
def make_message(message_id, body='body', headers=None, hidden=False):
35
29
if headers is None:
38
'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'),
44
message_headers.update(headers.items())
46
message.set_payload(body)
47
for key, value in message_headers.items():
49
return make_json_message(message_id, message.as_string(), hidden)
43
if 'in-reply-to' in headers:
44
message['replies'] = headers['in-reply-to']
52
48
def make_mime_message(message_id, body='body', headers=None, hidden=False,
53
49
attachment_type=None):
54
parts = MIMEMultipart()
55
parts.attach(MIMEText(body))
50
message = MIMEMultipart()
51
message.attach(MIMEText(body))
56
52
if attachment_type is not None:
57
53
attachment = Message()
58
54
attachment.set_payload('attactment data.')
59
55
attachment['Content-Type'] = attachment_type
60
56
attachment['Content-Disposition'] = 'attachment; filename="file.ext"'
61
parts.attach(attachment)
62
return make_message(message_id, parts.as_string(), headers, hidden)
65
class XXXForkedFakeService:
57
message.attach(attachment)
58
return make_message(message_id, message.get_payload(), headers, hidden)
61
def threaded_messages(messages):
65
for message in messages:
66
if message.get('replies') is None:
67
threads[message['message_id']] = [message]
70
pending.append(message)
71
for message in pending:
72
threads[message['replies']].append(message)
73
return threads.values()
77
"""A memory-backed message store."""
79
def __init__(self, messages):
81
self.messages = messages
83
def get_messages(self, archive_id, query_string):
84
"""Return matching messages.
86
:param archive_id: The archive to retrieve from.
87
:param query_string: Contains 'parameters', which is a JSON-format
88
string describing parameters.
90
query = parse_qs(query_string)
91
parameters = simplejson.loads(query['parameters'][0])
92
order = parameters.get('order')
93
messages = self.messages[archive_id]
95
if order not in SUPPORTED_ORDERS:
96
raise UnsupportedOrder
97
elif order.startswith('thread_'):
98
threaded = threaded_messages(messages)
100
if order == 'thread_subject':
101
threaded.sort(key=lambda t: t[0]['subject'])
102
if order == 'thread_oldest':
103
threaded.sort(key=lambda t: min(m['date'] for m in t))
104
if order == 'thread_newest':
105
threaded.sort(key=lambda t: max(m['date'] for m in t))
106
for thread in threaded:
107
messages.extend(thread)
109
messages.sort(key=lambda m: m[order])
110
display_type = parameters.get('display_type', 'all')
111
if display_type not in SUPPORTED_DISPLAY_TYPES:
112
raise UnsupportedDisplayType
114
for message in messages:
115
if (not parameters['include_hidden']
116
and message.get('hidden', False)):
119
if ('message_ids' in parameters
120
and message['message_id'] not in parameters['message_ids']):
122
message = dict(message)
123
if 'headers' in parameters:
125
(k, v) for k, v in message['headers'].iteritems()
126
if k in parameters['headers'])
127
message['headers'] = headers
128
max_body = parameters.get('max_body_length')
129
if display_type == 'headers-only':
131
elif (display_type == 'text-only'
132
and isinstance(message['body'], list)):
134
part.get_payload() for part in message['body']
135
if part.get_content_type() == 'text/plain']
136
message['body'] = '\n\n'.join(text_parts)
137
elif (display_type == 'all'
138
and isinstance(message['body'], list)):
139
parts = [str(part.get_payload()) for part in message['body']]
140
message['body'] = '\n\n'.join(parts)
141
if max_body is not None and display_type != 'headers-only':
142
message['body'] = message['body'][:max_body]
143
new_messages.append(message)
144
messages = new_messages
145
limit = parameters.get('limit', 100)
146
memo = parameters.get('memo')
147
message_id_indices = dict(
148
(m['message_id'], idx) for idx, m in enumerate(messages))
152
start = message_id_indices[memo.encode('rot13')]
154
previous_memo = messages[start - 1]['message_id'].encode('rot13')
157
end = min(start + limit, len(messages))
158
if end < len(messages):
159
next_memo = messages[end]['message_id'].encode('rot13')
162
messages = messages[start:end]
165
'messages': messages,
166
'next_memo': next_memo,
167
'previous_memo': previous_memo
172
class ForkedFakeService:
66
173
"""A Grackle service fake, as a ContextManager."""
68
def __init__(self, port, message_archives=None, write_logs=False):
175
def __init__(self, port, messages=None, write_logs=False):
71
178
:param port: The tcp port to use.
72
:param message_archives: A dict of lists of dicts representing
73
archives of messages. The outer dict represents the archive,
74
the list represents the list of messages for that archive.
179
:param messages: A dict of lists of dicts representing messages. The
180
outer dict represents the archive, the list represents the list of
181
messages for that archive.
75
182
:param write_logs: If true, log messages will be written to stdout.
79
if message_archives is None:
80
self.message_archives = {}
82
self.message_archives = message_archives
189
self.messages = messages
83
190
self.read_end, self.write_end = os.pipe()
84
191
self.write_logs = write_logs
87
def from_client(client, message_archives=None):
194
def from_client(client, messages=None):
88
195
"""Instantiate a ForkedFakeService from the client.
90
197
:param port: The client to provide service for.
91
:param message_archives: A dict of lists of dicts representing
92
archives of messages. The outer dict represents the archive,
93
the list represents the list of messages for that archive.
198
:param messages: A dict of lists of dicts representing messages. The
199
outer dict represents the archive, the list represents the list of
200
messages for that archive.
95
return ForkedFakeService(client.port, message_archives)
202
return ForkedFakeService(client.port, messages)
97
204
def is_ready(self):
98
205
"""Tell the parent process that the server is ready for writes."""
132
250
self.logger = logging.getLogger('http')
133
251
BaseHTTPRequestHandler.__init__(self, *args, **kwargs)
136
"""Create an archive or message on PUT."""
137
scheme, netloc, path, params, query_string, fragments = (
139
parts = path.split('/')
140
if parts[1] != 'archive':
141
# This is an unknonwn operation?
144
# This expected path is /archive/archive_id.
146
self.server.store.put_archive(parts[2])
147
self.send_response(httplib.CREATED)
150
except Exception, error:
152
httplib.BAD_REQUEST, error.__doc__)
154
# This expected path is /archive/archive_id/message_id.
156
message = self.rfile.read(int(self.headers['content-length']))
157
self.server.store.put_message(parts[2], parts[3], message)
158
self.send_response(httplib.CREATED)
162
self.send_error(httplib.BAD_REQUEST)
164
253
def do_POST(self):
165
"""Change a message on POST."""
166
scheme, netloc, path, params, query_string, fragments = (
168
parts = path.split('/')
169
if parts[1] != 'archive':
170
# This is an unknonwn operation?
173
# This expected path is /archive/archive_id/message_id.
175
# This expected path is /archive/archive_id/message_id.
176
response = self.server.store.hide_message(
177
parts[2], parts[3], query_string)
178
self.send_response(httplib.OK)
180
self.wfile.write(simplejson.dumps(response))
182
self.send_error(httplib.BAD_REQUEST)
254
"""Create a message on POST."""
255
message = self.rfile.read(int(self.headers['content-length']))
256
if message == 'This is a message':
257
self.send_response(httplib.CREATED)
261
self.send_error(httplib.BAD_REQUEST)
184
263
def do_GET(self):
185
264
"""Retrieve a list of messages on GET."""
205
288
self.logger.info(message)
208
class TestPutArchive(TestCase):
210
def test_put_archive(self):
211
client = GrackleClient('localhost', 8410)
212
message_archives = {}
213
with ForkedFakeService.from_client(client, message_archives):
214
client.put_archive('arch1')
215
response = client.get_messages('arch1')
216
self.assertEqual(0, len(response['messages']))
218
def test_put_archive_existing_archive(self):
219
client = GrackleClient('localhost', 8411)
220
message_archives = {'arch1': []}
221
with ForkedFakeService.from_client(client, message_archives):
222
with ExpectedException(ArchiveIdExists, ''):
223
client.put_archive('arch1')
226
291
class TestPutMessage(TestCase):
228
293
def test_put_message(self):
229
client = GrackleClient('localhost', 8420)
230
message_archives = {'arch1': []}
231
with ForkedFakeService.from_client(client, message_archives):
232
client.put_message('arch1', 'id1', StringIO('This is a message'))
233
response = client.get_messages('arch1')
234
self.assertEqual(1, len(response['messages']))
235
message = response['messages'][0]
236
self.assertEqual('id1', message['message_id'])
238
def test_put_message_without_archive(self):
239
client = GrackleClient('localhost', 8421)
240
message_archives = {'arch1': []}
241
with ForkedFakeService.from_client(client, message_archives):
294
client = GrackleClient('localhost', 8436)
295
with ForkedFakeService.from_client(client):
296
client.put_message('arch1', 'asdf', StringIO('This is a message'))
242
297
with ExpectedException(Exception, 'wtf'):
243
client.put_message('no-archive', 'id1', StringIO('message'))
298
client.put_message('arch1', 'asdf',
299
StringIO('This is not a message'))
246
302
class TestGetMessages(TestCase):
480
533
self.assertEqual('foo', first_message['message_id'])
481
534
self.assertEqual('me', first_message['headers']['From'])
482
535
self.assertEqual('you', first_message['headers']['To'])
483
self.assertEqual(archive['baz'][0]['body'], first_message['body'])
485
def test_date_range(self):
486
client = GrackleClient('localhost', 8448)
490
'foo', 'abcdefghi', headers={'date': '2011-12-31'}),
492
'bar', 'abcdefghi', headers={'date': '2012-01-01'}),
494
'qux', 'abcdefghi', headers={'date': '2012-01-15'}),
496
'naf', 'abcdefghi', headers={'date': '2012-01-31'}),
498
'doh', 'abcdefghi', headers={'date': '2012-02-01'}),
500
with ForkedFakeService.from_client(client, archive):
501
response = client.get_messages(
502
'baz', date_range='2012-01-01..2012-01-31')
503
ids = sorted(m['message_id'] for m in response['messages'])
504
self.assertEqual(['bar', 'naf', 'qux'], ids)
506
def test_date_range_unparsabledaterange(self):
507
client = GrackleClient('localhost', 8449)
508
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
509
with ForkedFakeService.from_client(client, archive):
510
with ExpectedException(UnparsableDateRange, ''):
511
client.get_messages('baz', date_range='2012-01-01')
513
def test_date_range_unparsabledaterange_missing_part(self):
514
client = GrackleClient('localhost', 8450)
515
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
516
with ForkedFakeService.from_client(client, archive):
517
with ExpectedException(UnparsableDateRange, ''):
518
client.get_messages('baz', date_range='2012-01-01..')
520
def test_date_range_unparsabledaterange_extra_part(self):
521
client = GrackleClient('localhost', 8451)
522
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
523
with ForkedFakeService.from_client(client, archive):
524
with ExpectedException(UnparsableDateRange, ''):
525
client.get_messages('baz', date_range='2012-01..12-02..12-03')
528
class TestHideMessages(TestCase):
530
def test_hide_message_true(self):
531
client = GrackleClient('localhost', 8470)
534
make_message('foo', hidden=False),
536
with ForkedFakeService.from_client(client, archive):
537
response = client.hide_message('baz', 'foo', hidden=True)
538
self.assertEqual('foo', response['message_id'])
539
self.assertIs(True, response['hidden'])
541
def test_hide_message_false(self):
542
client = GrackleClient('localhost', 8470)
545
make_message('foo', hidden=True),
547
with ForkedFakeService.from_client(client, archive):
548
response = client.hide_message('baz', 'foo', hidden=False)
549
self.assertEqual('foo', response['message_id'])
550
self.assertIs(False, response['hidden'])
537
'abcdefghi\n\nattactment data.', first_message['body'])