3
3
BaseHTTPRequestHandler,
5
from email.message import Message
6
from email.mime.multipart import MIMEMultipart
7
from email.mime.text import MIMEText
11
7
from signal import SIGKILL
13
9
from StringIO import StringIO
15
10
from unittest import TestCase
16
11
from urlparse import urlparse
12
from urlparse import parse_qs
18
14
from testtools import ExpectedException
20
16
from grackle.client import (
23
UnsupportedDisplayType,
26
from grackle.store import (
32
def make_message(message_id, body='body', headers=None, hidden=False):
35
headers['Message-Id'] = message_id
37
'message_id': message_id,
39
'thread_id': message_id,
40
'date': headers.get('date', '2005-01-01'),
41
'subject': headers.get('subject', 'subject'),
42
'author': headers.get('author', 'author'),
45
'replies': headers.get('in-reply-to', None),
51
def make_mime_message(message_id, body='body', headers=None, hidden=False,
52
attachment_type=None):
53
message = MIMEMultipart()
54
message.attach(MIMEText(body))
57
for key, value in headers.items():
59
if attachment_type is not None:
60
attachment = Message()
61
attachment.set_payload('attactment data.')
62
attachment['Content-Type'] = attachment_type
63
attachment['Content-Disposition'] = 'attachment; filename="file.ext"'
64
message.attach(attachment)
65
return make_json_message(message_id, message.as_string())
68
class ForkedFakeService:
69
"""A Grackle service fake, as a ContextManager."""
71
def __init__(self, port, message_archives=None, write_logs=False):
74
:param port: The tcp port to use.
75
:param message_archives: A dict of lists of dicts representing
76
archives of messages. The outer dict represents the archive,
77
the list represents the list of messages for that archive.
78
:param write_logs: If true, log messages will be written to stdout.
22
def threaded_messages(messages):
26
for message in messages:
27
if message.get('in_reply_to') is None:
28
threads[message['message_id']] = [message]
31
pending.append(message)
32
for message in pending:
33
threads[message['in_reply_to']].append(message)
34
return threads.values()
39
def __init__(self, messages):
40
self.messages = messages
42
def get_messages(self, archive_id, query_string):
43
query = parse_qs(query_string)
44
parameters = simplejson.loads(query['parameters'][0])
45
order = parameters.get('order')
46
messages = self.messages[archive_id]
47
if order is not None :
48
if order not in SUPPORTED_ORDERS:
49
raise UnsupportedOrder
50
elif order.startswith('thread_'):
51
threaded = threaded_messages(messages)
53
if order == 'thread_subject':
54
threaded.sort(key=lambda t: t[0]['subject'])
55
if order == 'thread_oldest':
56
threaded.sort(key=lambda t: min(m['date'] for m in t))
57
if order == 'thread_newest':
58
threaded.sort(key=lambda t: max(m['date'] for m in t))
59
for thread in threaded:
60
messages.extend(thread)
62
messages.sort(key=lambda m: m[order])
64
for message in messages:
66
not parameters['include_hidden']
67
and message.get('hidden', False)):
70
if ('message_ids' in parameters and
71
message['message_id'] not in parameters['message_ids']):
73
message = dict(message)
74
if 'headers' in parameters:
76
(k, v) for k, v in message['headers'].iteritems()
77
if k in parameters['headers'])
78
message['headers'] = headers
79
max_body = parameters.get('max_body_length')
80
if max_body is not None:
81
message['body'] = message['body'][:max_body]
82
new_messages.append(message)
83
messages = new_messages
84
limit = parameters.get('limit', 100)
85
memo = parameters.get('memo')
86
message_id_indices = dict(
87
(m['message_id'], idx) for idx, m in enumerate(messages))
91
start = message_id_indices[memo.encode('rot13')]
93
previous_memo = messages[start - 1]['message_id'].encode('rot13')
96
end = min(start + limit, len(messages))
97
if end < len(messages):
98
next_memo = messages[end]['message_id'].encode('rot13')
101
messages = messages[start:end]
104
'messages': messages,
105
'next_memo': next_memo,
106
'previous_memo': previous_memo
114
def __init__(self, port, messages=None):
82
if message_archives is None:
83
self.message_archives = {}
85
self.message_archives = message_archives
120
self.messages = messages
86
121
self.read_end, self.write_end = os.pipe()
87
self.write_logs = write_logs
90
def from_client(client, message_archives=None):
91
"""Instantiate a ForkedFakeService from the client.
93
:param port: The client to provide service for.
94
:param message_archives: A dict of lists of dicts representing
95
archives of messages. The outer dict represents the archive,
96
the list represents the list of messages for that archive.
98
return ForkedFakeService(client.port, message_archives)
124
def from_client(client, messages=None):
125
return ForkedFake(client.port, messages)
100
127
def is_ready(self):
101
"""Tell the parent process that the server is ready for writes."""
102
128
os.write(self.write_end, 'asdf')
104
130
def __enter__(self):
107
Fork and start a server in the child. Return when the server is ready
111
133
self.start_server()
116
138
def start_server(self):
117
"""Start the HTTP server."""
118
139
service = HTTPServer(('', self.port), FakeGrackleRequestHandler)
119
service.store = MemoryStore(self.message_archives)
120
for archive_id, messages in service.store.message_archives.iteritems():
140
service.store = GrackleStore(self.messages)
141
for archive_id, messages in service.store.messages.iteritems():
121
142
for message in messages:
122
143
message.setdefault('headers', {})
126
stream=sys.stderr, level=logging.INFO)
127
145
service.serve_forever()
129
147
def __exit__(self, exc_type, exc_val, traceback):
130
148
os.kill(self.pid, SIGKILL)
151
SUPPORTED_ORDERS = set(
152
['date', 'author', 'subject', 'thread_newest', 'thread_oldest',
133
156
class FakeGrackleRequestHandler(BaseHTTPRequestHandler):
134
"""A request handler that forwards to server.store."""
136
def __init__(self, *args, **kwargs):
137
"""Constructor. Sets up logging."""
138
self.logger = logging.getLogger('http')
139
BaseHTTPRequestHandler.__init__(self, *args, **kwargs)
141
158
def do_POST(self):
142
"""Create a message on POST."""
143
159
message = self.rfile.read(int(self.headers['content-length']))
144
scheme, netloc, path, params, query_string, fragments = (
146
parts = path.split('/')
147
if parts[1] == 'archive' and len(parts) == 4:
149
# This expected path is /archive/archive_id/message_id.
150
self.server.store.put_message(parts[2], parts[3], message)
151
self.send_response(httplib.CREATED)
155
self.send_error(httplib.BAD_REQUEST)
160
if message == 'This is a message':
161
self.send_response(httplib.CREATED)
165
self.send_error(httplib.BAD_REQUEST)
157
167
def do_GET(self):
158
"""Retrieve a list of messages on GET."""
159
168
scheme, netloc, path, params, query_string, fragments = (
160
169
urlparse(self.path))
161
170
parts = path.split('/')
166
175
self.send_response(httplib.OK)
167
176
self.end_headers()
168
177
self.wfile.write(simplejson.dumps(response))
169
except Exception, error:
171
httplib.BAD_REQUEST, error.__doc__)
178
except UnsupportedOrder:
179
self.send_response(httplib.BAD_REQUEST)
180
self.wfile.write('Unsupported order')
174
def log_message(self, format, *args):
175
"""Override log_message to use standard Python logging."""
176
message = "%s - - [%s] %s\n" % (
177
self.address_string(), self.log_date_time_string(), format % args)
178
self.logger.info(message)
181
184
class TestPutMessage(TestCase):
183
186
def test_put_message(self):
184
client = GrackleClient('localhost', 8420)
185
message_archives = {'arch1': []}
186
with ForkedFakeService.from_client(client, message_archives):
187
client.put_message('arch1', 'id1', StringIO('This is a message'))
188
response = client.get_messages('arch1')
189
self.assertEqual(1, len(response['messages']))
190
message = response['messages'][0]
191
self.assertEqual('id1', message['message_id'])
193
def test_put_message_without_archive(self):
194
client = GrackleClient('localhost', 8421)
195
message_archives = {'arch1': []}
196
with ForkedFakeService.from_client(client, message_archives):
187
client = GrackleClient('localhost', 8436)
188
with ForkedFake.from_client(client):
189
client.put_message('arch1', 'asdf', StringIO('This is a message'))
197
190
with ExpectedException(Exception, 'wtf'):
198
client.put_message('no-archive', 'id1', StringIO('message'))
191
client.put_message('arch1', 'asdf',
192
StringIO('This is not a message'))
201
195
class TestGetMessages(TestCase):
221
216
def test_get_messages_by_id(self):
222
217
client = GrackleClient('localhost', 8437)
224
'baz': [make_message('foo'), make_message('bar')]}
225
with ForkedFakeService.from_client(client, archive):
218
with ForkedFake.from_client(client,
220
[{'message_id': 'foo'},
221
{'message_id': 'bar'}]}):
226
222
response = client.get_messages('baz', message_ids=['foo'])
227
223
message, = response['messages']
228
224
self.assertEqual('foo', message['message_id'])
230
226
def test_get_messages_batching(self):
231
227
client = GrackleClient('localhost', 8438)
232
archive = {'baz': [make_message('foo'), make_message('bar')]}
233
with ForkedFakeService.from_client(client, archive):
228
with ForkedFake.from_client(client,
230
[{'message_id': 'foo'},
231
{'message_id': 'bar'}]}):
234
232
response = client.get_messages('baz', limit=1)
235
233
self.assertEqual(1, len(response['messages']))
236
234
messages = response['messages']
299
288
def test_get_messages_thread_newest_order(self):
300
289
client = GrackleClient('localhost', 8439)
303
make_message('bar', headers={'date': 'x'}),
304
make_message('qux', headers={'date': 'w'}),
305
make_message('foo', headers={'date': 'y',
306
'in-reply-to': 'bar'}),
307
make_message('baz', headers={'date': 'z',
308
'in-reply-to': 'qux'}),
310
with ForkedFakeService.from_client(client, archive):
290
with ForkedFake.from_client(client, {'baz': [
291
{'message_id': 'bar', 'date': 'x'},
292
{'message_id': 'qux', 'date': 'w'},
293
{'message_id': 'foo', 'date': 'y', 'in_reply_to': 'bar'},
294
{'message_id': 'baz', 'date': 'z', 'in_reply_to': 'qux'},
311
296
response = client.get_messages('baz', order='date')
312
297
self.assertIDOrder(
313
298
['qux', 'bar', 'foo', 'baz'], response['messages'])
318
303
def test_get_messages_unsupported_order(self):
319
304
client = GrackleClient('localhost', 8439)
322
make_message('foo', headers={'date': '2011-03-25'}),
323
make_message('foo', headers={'date': '2011-03-24'}),
325
with ForkedFakeService.from_client(client, archive):
326
with ExpectedException(UnsupportedOrder, ''):
305
with ForkedFake.from_client(client,
306
{'baz': [{'message_id': 'foo', 'date': '2011-03-25'},
307
{'message_id': 'bar', 'date': '2011-03-24'}]}):
308
with ExpectedException(UnsupportedOrder):
327
309
client.get_messages('baz', order='nonsense')
329
311
def test_get_messages_headers_no_headers(self):
330
312
client = GrackleClient('localhost', 8440)
331
archive = {'baz': [make_message('foo')]}
332
with ForkedFakeService.from_client(client, archive):
313
with ForkedFake.from_client(client,
315
{'message_id': 'foo'}
333
317
response = client.get_messages('baz', headers=[
334
318
'Subject', 'Date', 'X-Launchpad-Message-Rationale'])
335
319
first_message = response['messages'][0]
362
347
def test_get_messages_max_body_length(self):
363
348
client = GrackleClient('localhost', 8443)
364
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
365
with ForkedFakeService.from_client(client, archive):
349
with ForkedFake.from_client(client,
351
{'message_id': 'foo', 'body': u'abcdefghi'}
366
353
response = client.get_messages('baz', max_body_length=3)
367
354
first_message = response['messages'][0]
368
355
self.assertEqual('abc', first_message['body'])
370
357
def test_include_hidden(self):
371
358
client = GrackleClient('localhost', 8444)
374
make_message('foo', hidden=True),
375
make_message('bar', hidden=False),
377
with ForkedFakeService.from_client(client, archive):
359
with ForkedFake.from_client(client,
361
{'message_id': 'foo', 'hidden': True},
362
{'message_id': 'bar', 'hidden': False}
378
364
response = client.get_messages('baz', include_hidden=True)
379
365
self.assertMessageIDs(['bar', 'foo'], response['messages'])
380
366
response = client.get_messages('baz', include_hidden=False)
381
367
self.assertMessageIDs(['bar'], response['messages'])
383
def test_display_type_unknown_value(self):
384
client = GrackleClient('localhost', 8445)
385
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
386
with ForkedFakeService.from_client(client, archive):
387
with ExpectedException(UnsupportedDisplayType, ''):
388
client.get_messages('baz', display_type='unknown')
390
def test_display_type_headers_only(self):
391
client = GrackleClient('localhost', 8446)
394
make_message('foo', body=u'abcdefghi',
395
headers={'From': 'me', 'To': 'you'})]}
396
with ForkedFakeService.from_client(client, archive):
397
response = client.get_messages('baz', display_type='headers-only')
398
first_message = response['messages'][0]
399
self.assertEqual('foo', first_message['message_id'])
401
{'From': 'me', 'Message-Id': 'foo', 'To': 'you'},
402
first_message['headers'])
403
self.assertNotIn('body', first_message)
405
def test_display_type_text_only(self):
406
client = GrackleClient('localhost', 8446)
411
headers={'From': 'me', 'To': 'you'},
412
attachment_type='text/x-diff')]}
413
with ForkedFakeService.from_client(client, archive):
414
response = client.get_messages('baz', display_type='text-only')
415
first_message = response['messages'][0]
416
self.assertEqual('foo', first_message['message_id'])
417
self.assertEqual('me', first_message['headers']['From'])
418
self.assertEqual('you', first_message['headers']['To'])
419
self.assertEqual(archive['baz'][0]['body'], first_message['body'])
421
def test_display_type_all(self):
422
client = GrackleClient('localhost', 8447)
427
headers={'From': 'me', 'To': 'you'},
428
attachment_type='text/x-diff')]}
429
with ForkedFakeService.from_client(client, archive):
430
response = client.get_messages('baz', display_type='all')
431
first_message = response['messages'][0]
432
self.assertEqual('foo', first_message['message_id'])
433
self.assertEqual('me', first_message['headers']['From'])
434
self.assertEqual('you', first_message['headers']['To'])
435
self.assertEqual(archive['baz'][0]['body'], first_message['body'])
437
def test_date_range(self):
438
client = GrackleClient('localhost', 8448)
442
'foo', 'abcdefghi', headers={'date': '2011-12-31'}),
444
'bar', 'abcdefghi', headers={'date': '2012-01-01'}),
446
'qux', 'abcdefghi', headers={'date': '2012-01-15'}),
448
'naf', 'abcdefghi', headers={'date': '2012-01-31'}),
450
'doh', 'abcdefghi', headers={'date': '2012-02-01'}),
452
with ForkedFakeService.from_client(client, archive):
453
response = client.get_messages(
454
'baz', date_range='2012-01-01..2012-01-31')
455
ids = sorted(m['message_id'] for m in response['messages'])
456
self.assertEqual(['bar', 'naf', 'qux'], ids)
458
def test_date_range_unparsabledaterange(self):
459
client = GrackleClient('localhost', 8449)
460
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
461
with ForkedFakeService.from_client(client, archive):
462
with ExpectedException(UnparsableDateRange, ''):
463
client.get_messages('baz', date_range='2012-01-01')
465
def test_date_range_unparsabledaterange_missing_part(self):
466
client = GrackleClient('localhost', 8450)
467
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
468
with ForkedFakeService.from_client(client, archive):
469
with ExpectedException(UnparsableDateRange, ''):
470
client.get_messages('baz', date_range='2012-01-01..')
472
def test_date_range_unparsabledaterange_extra_part(self):
473
client = GrackleClient('localhost', 8451)
474
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
475
with ForkedFakeService.from_client(client, archive):
476
with ExpectedException(UnparsableDateRange, ''):
477
client.get_messages('baz', date_range='2012-01..12-02..12-03')