1
from BaseHTTPServer import (
3
BaseHTTPRequestHandler,
3
5
from email.message import Message
4
6
from email.mime.multipart import MIMEMultipart
5
7
from email.mime.text import MIMEText
11
from signal import SIGKILL
6
13
from StringIO import StringIO
7
15
from unittest import TestCase
16
from urlparse import urlparse
17
from urlparse import parse_qs
9
19
from testtools import ExpectedException
11
from grackle.client import GrackleClient
12
from grackle.error import (
21
from grackle.client import (
15
23
UnsupportedDisplayType,
18
from grackle.service import ForkedFakeService
19
from grackle.store import make_json_message
22
def make_message(message_id, body='body', headers=None, hidden=False):
28
def make_message(message_id, body='body text', headers=None, hidden=False):
23
29
if headers is None:
26
'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'),
32
message_headers.update(headers.items())
34
message.set_payload(body)
35
for key, value in message_headers.items():
37
return make_json_message(message_id, message.as_string(), hidden)
40
def make_mime_message(message_id, body='body', headers=None, hidden=False,
43
if 'in-reply-to' in headers:
44
message['in_reply_to'] = headers['in-reply-to']
48
def make_mime_message(message_id, text, headers=None, hidden=False,
41
49
attachment_type=None):
42
parts = MIMEMultipart()
43
parts.attach(MIMEText(body))
50
message = MIMEMultipart()
51
message.attach(MIMEText(text))
52
message['Message-Id'] = message_id
53
if headers is not None:
54
for k, v in headers.items():
44
56
if attachment_type is not None:
45
57
attachment = Message()
46
58
attachment.set_payload('attactment data.')
47
59
attachment['Content-Type'] = attachment_type
48
attachment['Content-Disposition'] = 'attachment; filename="file.ext"'
49
parts.attach(attachment)
50
return make_message(message_id, parts.as_string(), headers, hidden)
53
class TestPutArchive(TestCase):
55
def test_put_archive(self):
56
client = GrackleClient('localhost', 8410)
58
with ForkedFakeService.from_client(client, message_archives):
59
client.put_archive('arch1')
60
response = client.get_messages('arch1')
61
self.assertEqual(0, len(response['messages']))
63
def test_put_archive_existing_archive(self):
64
client = GrackleClient('localhost', 8411)
65
message_archives = {'arch1': []}
66
with ForkedFakeService.from_client(client, message_archives):
67
with ExpectedException(ArchiveIdExists, ''):
68
client.put_archive('arch1')
60
attachment['Content-Disposition'] = (
61
'attachment; filename="file.ext"')
62
message.attach(attachment)
64
'message_id': message_id,
65
'headers': dict(message.items()),
66
'body': message.get_payload(),
71
def threaded_messages(messages):
75
for message in messages:
76
if message.get('in_reply_to') is None:
77
threads[message['message_id']] = [message]
80
pending.append(message)
81
for message in pending:
82
threads[message['in_reply_to']].append(message)
83
return threads.values()
87
"""A memory-backed message store."""
89
def __init__(self, messages):
91
self.messages = messages
93
def get_messages(self, archive_id, query_string):
94
"""Return matching messages.
96
:param archive_id: The archive to retrieve from.
97
:param query_string: Contains 'parameters', which is a JSON-format
98
string describing parameters.
100
query = parse_qs(query_string)
101
parameters = simplejson.loads(query['parameters'][0])
102
order = parameters.get('order')
103
messages = self.messages[archive_id]
104
if order is not None:
105
if order not in SUPPORTED_ORDERS:
106
raise UnsupportedOrder
107
elif order.startswith('thread_'):
108
threaded = threaded_messages(messages)
110
if order == 'thread_subject':
111
threaded.sort(key=lambda t: t[0]['subject'])
112
if order == 'thread_oldest':
113
threaded.sort(key=lambda t: min(m['date'] for m in t))
114
if order == 'thread_newest':
115
threaded.sort(key=lambda t: max(m['date'] for m in t))
116
for thread in threaded:
117
messages.extend(thread)
119
messages.sort(key=lambda m: m[order])
120
display_type = parameters.get('display_type', 'all')
121
if display_type not in SUPPORTED_DISPLAY_TYPES:
122
raise UnsupportedDisplayType
124
for message in messages:
125
if (not parameters['include_hidden']
126
and message.get('hidden', False)):
129
if ('message_ids' in parameters
130
and message['message_id'] not in parameters['message_ids']):
132
message = dict(message)
133
if 'headers' in parameters:
135
(k, v) for k, v in message['headers'].iteritems()
136
if k in parameters['headers'])
137
message['headers'] = headers
138
max_body = parameters.get('max_body_length')
139
if display_type == 'headers-only':
141
elif (display_type == 'text-only'
142
and isinstance(message['body'], list)):
144
part.get_payload() for part in message['body']
145
if part.get_content_type() == 'text/plain']
146
message['body'] = '\n\n'.join(text_parts)
147
elif (display_type == 'all'
148
and 'body' in message
149
and isinstance(message['body'], list)):
150
parts = [str(part.get_payload()) for part in message['body']]
151
message['body'] = '\n\n'.join(parts)
152
if max_body is not None and display_type != 'headers-only':
153
message['body'] = message['body'][:max_body]
154
new_messages.append(message)
155
messages = new_messages
156
limit = parameters.get('limit', 100)
157
memo = parameters.get('memo')
158
message_id_indices = dict(
159
(m['message_id'], idx) for idx, m in enumerate(messages))
163
start = message_id_indices[memo.encode('rot13')]
165
previous_memo = messages[start - 1]['message_id'].encode('rot13')
168
end = min(start + limit, len(messages))
169
if end < len(messages):
170
next_memo = messages[end]['message_id'].encode('rot13')
173
messages = messages[start:end]
176
'messages': messages,
177
'next_memo': next_memo,
178
'previous_memo': previous_memo
183
class ForkedFakeService:
184
"""A Grackle service fake, as a ContextManager."""
186
def __init__(self, port, messages=None, write_logs=False):
189
:param port: The tcp port to use.
190
:param messages: A dict of lists of dicts representing messages. The
191
outer dict represents the archive, the list represents the list of
192
messages for that archive.
193
:param write_logs: If true, log messages will be written to stdout.
200
self.messages = messages
201
self.read_end, self.write_end = os.pipe()
202
self.write_logs = write_logs
205
def from_client(client, messages=None):
206
"""Instantiate a ForkedFakeService from the client.
208
:param port: The client to provide service for.
209
:param messages: A dict of lists of dicts representing messages. The
210
outer dict represents the archive, the list represents the list of
211
messages for that archive.
213
return ForkedFakeService(client.port, messages)
216
"""Tell the parent process that the server is ready for writes."""
217
os.write(self.write_end, 'asdf')
222
Fork and start a server in the child. Return when the server is ready
228
os.read(self.read_end, 1)
231
def start_server(self):
232
"""Start the HTTP server."""
233
service = HTTPServer(('', self.port), FakeGrackleRequestHandler)
234
service.store = GrackleStore(self.messages)
235
for archive_id, messages in service.store.messages.iteritems():
236
for message in messages:
237
message.setdefault('headers', {})
241
stream=sys.stderr, level=logging.INFO)
242
service.serve_forever()
244
def __exit__(self, exc_type, exc_val, traceback):
245
os.kill(self.pid, SIGKILL)
248
SUPPORTED_DISPLAY_TYPES = set(['all', 'text-only', 'headers-only'])
251
SUPPORTED_ORDERS = set(
252
['date', 'author', 'subject', 'thread_newest', 'thread_oldest',
256
class FakeGrackleRequestHandler(BaseHTTPRequestHandler):
257
"""A request handler that forwards to server.store."""
259
def __init__(self, *args, **kwargs):
260
"""Constructor. Sets up logging."""
261
self.logger = logging.getLogger('http')
262
BaseHTTPRequestHandler.__init__(self, *args, **kwargs)
265
"""Create a message on POST."""
266
message = self.rfile.read(int(self.headers['content-length']))
267
if message == 'This is a message':
268
self.send_response(httplib.CREATED)
272
self.send_error(httplib.BAD_REQUEST)
275
"""Retrieve a list of messages on GET."""
276
scheme, netloc, path, params, query_string, fragments = (
278
parts = path.split('/')
279
if parts[1] == 'archive':
281
response = self.server.store.get_messages(
282
parts[2], query_string)
283
self.send_response(httplib.OK)
285
self.wfile.write(simplejson.dumps(response))
286
except UnsupportedOrder:
288
httplib.BAD_REQUEST, UnsupportedOrder.__doc__)
290
except UnsupportedDisplayType:
292
httplib.BAD_REQUEST, UnsupportedDisplayType.__doc__)
295
def log_message(self, format, *args):
296
"""Override log_message to use standard Python logging."""
297
message = "%s - - [%s] %s\n" % (
298
self.address_string(), self.log_date_time_string(), format % args)
299
self.logger.info(message)
71
302
class TestPutMessage(TestCase):
73
304
def test_put_message(self):
74
client = GrackleClient('localhost', 8420)
75
message_archives = {'arch1': []}
76
with ForkedFakeService.from_client(client, message_archives):
77
client.put_message('arch1', 'id1', StringIO('This is a message'))
78
response = client.get_messages('arch1')
79
self.assertEqual(1, len(response['messages']))
80
message = response['messages'][0]
81
self.assertEqual('id1', message['message_id'])
83
def test_put_message_without_archive(self):
84
client = GrackleClient('localhost', 8421)
85
message_archives = {'arch1': []}
86
with ForkedFakeService.from_client(client, message_archives):
305
client = GrackleClient('localhost', 8436)
306
with ForkedFakeService.from_client(client):
307
client.put_message('arch1', 'asdf', StringIO('This is a message'))
87
308
with ExpectedException(Exception, 'wtf'):
88
client.put_message('no-archive', 'id1', StringIO('message'))
309
client.put_message('arch1', 'asdf',
310
StringIO('This is not a message'))
91
313
class TestGetMessages(TestCase):
277
486
def test_display_type_unknown_value(self):
278
487
client = GrackleClient('localhost', 8445)
279
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
280
with ForkedFakeService.from_client(client, archive):
488
with ForkedFakeService.from_client(client,
490
make_message('foo', body=u'abcdefghi')]}):
281
491
with ExpectedException(UnsupportedDisplayType, ''):
282
492
client.get_messages('baz', display_type='unknown')
284
494
def test_display_type_headers_only(self):
285
495
client = GrackleClient('localhost', 8446)
496
with ForkedFakeService.from_client(client,
288
498
make_message('foo', body=u'abcdefghi',
289
headers={'From': 'me', 'To': 'you'})]}
290
with ForkedFakeService.from_client(client, archive):
499
headers={'From': 'me', 'To': 'you'})]}):
291
500
response = client.get_messages('baz', display_type='headers-only')
292
501
first_message = response['messages'][0]
293
502
self.assertEqual('foo', first_message['message_id'])
294
503
self.assertEqual(
295
archive['baz'][0]['headers'], first_message['headers'])
504
{'From': 'me', 'Message-Id': 'foo', 'To': 'you'},
505
first_message['headers'])
296
506
self.assertNotIn('body', first_message)
298
508
def test_display_type_text_only(self):
299
509
client = GrackleClient('localhost', 8446)
510
with ForkedFakeService.from_client(client,
302
512
make_mime_message(
303
513
'foo', 'abcdefghi',
304
514
headers={'From': 'me', 'To': 'you'},
305
attachment_type='text/x-diff')]}
306
with ForkedFakeService.from_client(client, archive):
515
attachment_type='text/x-diff')
307
517
response = client.get_messages('baz', display_type='text-only')
308
518
first_message = response['messages'][0]
309
519
self.assertEqual('foo', first_message['message_id'])
310
520
self.assertEqual('me', first_message['headers']['From'])
311
521
self.assertEqual('you', first_message['headers']['To'])
312
self.assertEqual(archive['baz'][0]['body'], first_message['body'])
522
self.assertEqual('abcdefghi', first_message['body'])
314
524
def test_display_type_all(self):
315
525
client = GrackleClient('localhost', 8447)
526
with ForkedFakeService.from_client(client,
318
528
make_mime_message(
319
529
'foo', 'abcdefghi',
320
530
headers={'From': 'me', 'To': 'you'},
321
attachment_type='text/x-diff')]}
322
with ForkedFakeService.from_client(client, archive):
531
attachment_type='text/x-diff')
323
533
response = client.get_messages('baz', display_type='all')
324
534
first_message = response['messages'][0]
325
535
self.assertEqual('foo', first_message['message_id'])
326
536
self.assertEqual('me', first_message['headers']['From'])
327
537
self.assertEqual('you', first_message['headers']['To'])
328
self.assertEqual(archive['baz'][0]['body'], first_message['body'])
330
def test_date_range(self):
331
client = GrackleClient('localhost', 8448)
335
'foo', 'abcdefghi', headers={'date': '2011-12-31'}),
337
'bar', 'abcdefghi', headers={'date': '2012-01-01'}),
339
'qux', 'abcdefghi', headers={'date': '2012-01-15'}),
341
'naf', 'abcdefghi', headers={'date': '2012-01-31'}),
343
'doh', 'abcdefghi', headers={'date': '2012-02-01'}),
345
with ForkedFakeService.from_client(client, archive):
346
response = client.get_messages(
347
'baz', date_range='2012-01-01..2012-01-31')
348
ids = sorted(m['message_id'] for m in response['messages'])
349
self.assertEqual(['bar', 'naf', 'qux'], ids)
351
def test_date_range_unparsabledaterange(self):
352
client = GrackleClient('localhost', 8449)
353
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
354
with ForkedFakeService.from_client(client, archive):
355
with ExpectedException(UnparsableDateRange, ''):
356
client.get_messages('baz', date_range='2012-01-01')
358
def test_date_range_unparsabledaterange_missing_part(self):
359
client = GrackleClient('localhost', 8450)
360
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
361
with ForkedFakeService.from_client(client, archive):
362
with ExpectedException(UnparsableDateRange, ''):
363
client.get_messages('baz', date_range='2012-01-01..')
365
def test_date_range_unparsabledaterange_extra_part(self):
366
client = GrackleClient('localhost', 8451)
367
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
368
with ForkedFakeService.from_client(client, archive):
369
with ExpectedException(UnparsableDateRange, ''):
370
client.get_messages('baz', date_range='2012-01..12-02..12-03')
373
class TestHideMessages(TestCase):
375
def test_hide_message_true(self):
376
client = GrackleClient('localhost', 8470)
379
make_message('foo', hidden=False),
381
with ForkedFakeService.from_client(client, archive):
382
response = client.hide_message('baz', 'foo', hidden=True)
383
self.assertEqual('foo', response['message_id'])
384
self.assertIs(True, response['hidden'])
386
def test_hide_message_false(self):
387
client = GrackleClient('localhost', 8470)
390
make_message('foo', hidden=True),
392
with ForkedFakeService.from_client(client, archive):
393
response = client.hide_message('baz', 'foo', hidden=False)
394
self.assertEqual('foo', response['message_id'])
395
self.assertIs(False, response['hidden'])
539
'abcdefghi\n\nattactment data.', first_message['body'])