1
from BaseHTTPServer import (
3
BaseHTTPRequestHandler,
8
from signal import SIGKILL
3
from email.message import Message
4
from email.mime.multipart import MIMEMultipart
5
from email.mime.text import MIMEText
10
6
from StringIO import StringIO
12
7
from unittest import TestCase
13
from urlparse import urlparse
14
from urlparse import parse_qs
16
9
from testtools import ExpectedException
18
from grackle.client import (
11
from grackle.client import GrackleClient
12
from grackle.error import (
15
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
120
class ForkedFakeService:
121
"""A Grackle service fake, as a ContextManager."""
123
def __init__(self, port, messages=None, write_logs=False):
126
:param port: The tcp port to use.
127
:param messages: A dict of lists of dicts representing messages. The
128
outer dict represents the archive, the list represents the list of
129
messages for that archive.
130
:param write_logs: If true, log messages will be written to stdout.
137
self.messages = messages
138
self.read_end, self.write_end = os.pipe()
139
self.write_logs = write_logs
142
def from_client(client, messages=None):
143
"""Instantiate a ForkedFakeService from the client.
145
:param port: The client to provide service for.
146
:param messages: A dict of lists of dicts representing messages. The
147
outer dict represents the archive, the list represents the list of
148
messages for that archive.
150
return ForkedFakeService(client.port, messages)
153
"""Tell the parent process that the server is ready for writes."""
154
os.write(self.write_end, 'asdf')
159
Fork and start a server in the child. Return when the server is ready
165
os.read(self.read_end, 1)
168
def start_server(self):
169
"""Start the HTTP server."""
170
service = HTTPServer(('', self.port), FakeGrackleRequestHandler)
171
service.store = GrackleStore(self.messages)
172
for archive_id, messages in service.store.messages.iteritems():
173
for message in messages:
174
message.setdefault('headers', {})
178
stream=sys.stderr, level=logging.INFO)
179
service.serve_forever()
181
def __exit__(self, exc_type, exc_val, traceback):
182
os.kill(self.pid, SIGKILL)
185
SUPPORTED_ORDERS = set(
186
['date', 'author', 'subject', 'thread_newest', 'thread_oldest',
190
class FakeGrackleRequestHandler(BaseHTTPRequestHandler):
191
"""A request handler that forwards to server.store."""
193
def __init__(self, *args, **kwargs):
194
"""Constructor. Sets up logging."""
195
self.logger = logging.getLogger('http')
196
BaseHTTPRequestHandler.__init__(self, *args, **kwargs)
199
"""Create a message on POST."""
200
message = self.rfile.read(int(self.headers['content-length']))
201
if message == 'This is a message':
202
self.send_response(httplib.CREATED)
206
self.send_error(httplib.BAD_REQUEST)
209
"""Retrieve a list of messages on GET."""
210
scheme, netloc, path, params, query_string, fragments = (
212
parts = path.split('/')
213
if parts[1] == 'archive':
215
response = self.server.store.get_messages(
216
parts[2], query_string)
217
self.send_response(httplib.OK)
219
self.wfile.write(simplejson.dumps(response))
220
except UnsupportedOrder:
221
self.send_response(httplib.BAD_REQUEST)
222
self.wfile.write('Unsupported order')
225
def log_message(self, format, *args):
226
"""Override log_message to use standard Python logging."""
227
message = "%s - - [%s] %s\n" % (
228
self.address_string(), self.log_date_time_string(), format % args)
229
self.logger.info(message)
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')
232
71
class TestPutMessage(TestCase):
234
73
def test_put_message(self):
235
client = GrackleClient('localhost', 8436)
236
with ForkedFakeService.from_client(client):
237
client.put_message('arch1', 'asdf', StringIO('This is a message'))
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):
238
87
with ExpectedException(Exception, 'wtf'):
239
client.put_message('arch1', 'asdf',
240
StringIO('This is not a message'))
88
client.put_message('no-archive', 'id1', StringIO('message'))
243
91
class TestGetMessages(TestCase):
395
256
def test_get_messages_max_body_length(self):
396
257
client = GrackleClient('localhost', 8443)
397
with ForkedFakeService.from_client(client,
399
{'message_id': 'foo', 'body': u'abcdefghi'}
258
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
259
with ForkedFakeService.from_client(client, archive):
401
260
response = client.get_messages('baz', max_body_length=3)
402
261
first_message = response['messages'][0]
403
262
self.assertEqual('abc', first_message['body'])
405
264
def test_include_hidden(self):
406
265
client = GrackleClient('localhost', 8444)
407
with ForkedFakeService.from_client(client,
409
{'message_id': 'foo', 'hidden': True},
410
{'message_id': 'bar', 'hidden': False}
268
make_message('foo', hidden=True),
269
make_message('bar', hidden=False),
271
with ForkedFakeService.from_client(client, archive):
412
272
response = client.get_messages('baz', include_hidden=True)
413
273
self.assertMessageIDs(['bar', 'foo'], response['messages'])
414
274
response = client.get_messages('baz', include_hidden=False)
415
275
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'])