3
from email.message import Message
4
from email.mime.multipart import MIMEMultipart
5
from email.mime.text import MIMEText
1
from BaseHTTPServer import (
3
BaseHTTPRequestHandler,
8
from signal import SIGKILL
6
10
from StringIO import StringIO
7
12
from unittest import TestCase
13
from urlparse import urlparse
14
from urlparse import parse_qs
9
16
from testtools import ExpectedException
11
from grackle.client import GrackleClient
12
from grackle.error import (
15
UnsupportedDisplayType,
18
from grackle.client import (
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):
26
'Message-Id': message_id,
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,
41
attachment_type=None):
42
parts = MIMEMultipart()
43
parts.attach(MIMEText(body))
44
if attachment_type is not None:
45
attachment = Message()
46
attachment.set_payload('attactment data.')
47
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')
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
121
"""A Grackle service fake, as a ContextManager."""
123
def __init__(self, port, messages=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.
129
:param write_logs: If true, log messages will be written to stdout.
136
self.messages = messages
137
self.read_end, self.write_end = os.pipe()
138
self.write_logs = write_logs
141
def from_client(client, messages=None):
142
"""Instantiate a ForkedFake 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.
149
return ForkedFake(client.port, messages)
152
"""Tell the parent process that the server is ready for writes."""
153
os.write(self.write_end, 'asdf')
158
Fork and start a server in the child. Return when the server is ready
164
os.read(self.read_end, 1)
167
def start_server(self):
168
"""Start the HTTP server."""
169
service = HTTPServer(('', self.port), FakeGrackleRequestHandler)
170
service.store = GrackleStore(self.messages)
171
for archive_id, messages in service.store.messages.iteritems():
172
for message in messages:
173
message.setdefault('headers', {})
177
stream=sys.stderr, level=logging.INFO)
178
service.serve_forever()
180
def __exit__(self, exc_type, exc_val, traceback):
181
os.kill(self.pid, SIGKILL)
184
SUPPORTED_ORDERS = set(
185
['date', 'author', 'subject', 'thread_newest', 'thread_oldest',
189
class FakeGrackleRequestHandler(BaseHTTPRequestHandler):
190
"""A request handler that forwards to server.store."""
192
def __init__(self, *args, **kwargs):
193
"""Constructor. Sets up logging."""
194
self.logger = logging.getLogger('http')
195
BaseHTTPRequestHandler.__init__(self, *args, **kwargs)
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)
208
"""Retrieve a list of messages on GET."""
209
scheme, netloc, path, params, query_string, fragments = (
211
parts = path.split('/')
212
if parts[1] == 'archive':
214
response = self.server.store.get_messages(
215
parts[2], query_string)
216
self.send_response(httplib.OK)
218
self.wfile.write(simplejson.dumps(response))
219
except UnsupportedOrder:
220
self.send_response(httplib.BAD_REQUEST)
221
self.wfile.write('Unsupported order')
224
def log_message(self, format, *args):
225
"""Override log_message to use standard Python logging."""
226
message = "%s - - [%s] %s\n" % (
227
self.address_string(), self.log_date_time_string(), format % args)
228
self.logger.info(message)
71
231
class TestPutMessage(TestCase):
73
233
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):
234
client = GrackleClient('localhost', 8436)
235
with ForkedFake.from_client(client):
236
client.put_message('arch1', 'asdf', StringIO('This is a message'))
87
237
with ExpectedException(Exception, 'wtf'):
88
client.put_message('no-archive', 'id1', StringIO('message'))
238
client.put_message('arch1', 'asdf',
239
StringIO('This is not a message'))
91
242
class TestGetMessages(TestCase):
256
394
def test_get_messages_max_body_length(self):
257
395
client = GrackleClient('localhost', 8443)
258
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
259
with ForkedFakeService.from_client(client, archive):
396
with ForkedFake.from_client(client,
398
{'message_id': 'foo', 'body': u'abcdefghi'}
260
400
response = client.get_messages('baz', max_body_length=3)
261
401
first_message = response['messages'][0]
262
402
self.assertEqual('abc', first_message['body'])
264
404
def test_include_hidden(self):
265
405
client = GrackleClient('localhost', 8444)
268
make_message('foo', hidden=True),
269
make_message('bar', hidden=False),
271
with ForkedFakeService.from_client(client, archive):
406
with ForkedFake.from_client(client,
408
{'message_id': 'foo', 'hidden': True},
409
{'message_id': 'bar', 'hidden': False}
272
411
response = client.get_messages('baz', include_hidden=True)
273
412
self.assertMessageIDs(['bar', 'foo'], response['messages'])
274
413
response = client.get_messages('baz', include_hidden=False)
275
414
self.assertMessageIDs(['bar'], response['messages'])
277
def test_display_type_unknown_value(self):
278
client = GrackleClient('localhost', 8445)
279
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
280
with ForkedFakeService.from_client(client, archive):
281
with ExpectedException(UnsupportedDisplayType, ''):
282
client.get_messages('baz', display_type='unknown')
284
def test_display_type_headers_only(self):
285
client = GrackleClient('localhost', 8446)
288
make_message('foo', body=u'abcdefghi',
289
headers={'From': 'me', 'To': 'you'})]}
290
with ForkedFakeService.from_client(client, archive):
291
response = client.get_messages('baz', display_type='headers-only')
292
first_message = response['messages'][0]
293
self.assertEqual('foo', first_message['message_id'])
295
archive['baz'][0]['headers'], first_message['headers'])
296
self.assertNotIn('body', first_message)
298
def test_display_type_text_only(self):
299
client = GrackleClient('localhost', 8446)
304
headers={'From': 'me', 'To': 'you'},
305
attachment_type='text/x-diff')]}
306
with ForkedFakeService.from_client(client, archive):
307
response = client.get_messages('baz', display_type='text-only')
308
first_message = response['messages'][0]
309
self.assertEqual('foo', first_message['message_id'])
310
self.assertEqual('me', first_message['headers']['From'])
311
self.assertEqual('you', first_message['headers']['To'])
312
self.assertEqual(archive['baz'][0]['body'], first_message['body'])
314
def test_display_type_all(self):
315
client = GrackleClient('localhost', 8447)
320
headers={'From': 'me', 'To': 'you'},
321
attachment_type='text/x-diff')]}
322
with ForkedFakeService.from_client(client, archive):
323
response = client.get_messages('baz', display_type='all')
324
first_message = response['messages'][0]
325
self.assertEqual('foo', first_message['message_id'])
326
self.assertEqual('me', first_message['headers']['From'])
327
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'])