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 (
31
def make_message(message_id, body='body', headers=None, hidden=False):
34
headers['Message-Id'] = message_id
36
'message_id': message_id,
38
'thread_id': message_id,
39
'date': headers.get('date', '2005-01-01'),
40
'subject': headers.get('subject', 'subject'),
41
'author': headers.get('author', 'author'),
44
'replies': headers.get('in-reply-to', None),
50
def make_mime_message(message_id, body='body', headers=None, hidden=False,
51
attachment_type=None):
52
message = MIMEMultipart()
53
message.attach(MIMEText(body))
54
if attachment_type is not None:
55
attachment = Message()
56
attachment.set_payload('attactment data.')
57
attachment['Content-Type'] = attachment_type
58
attachment['Content-Disposition'] = 'attachment; filename="file.ext"'
59
message.attach(attachment)
60
return make_message(message_id, message.get_payload(), headers, hidden)
63
class ForkedFakeService:
64
"""A Grackle service fake, as a ContextManager."""
66
def __init__(self, port, message_archives=None, write_logs=False):
69
:param port: The tcp port to use.
70
:param message_archives: A dict of lists of dicts representing
71
archives of messages. The outer dict represents the archive,
72
the list represents the list of messages for that archive.
73
: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):
77
if message_archives is None:
78
self.message_archives = {}
80
self.message_archives = message_archives
117
self.messages = messages
81
118
self.read_end, self.write_end = os.pipe()
82
self.write_logs = write_logs
85
def from_client(client, message_archives=None):
86
"""Instantiate a ForkedFakeService from the client.
88
:param port: The client to provide service for.
89
:param message_archives: A dict of lists of dicts representing
90
archives of messages. The outer dict represents the archive,
91
the list represents the list of messages for that archive.
93
return ForkedFakeService(client.port, message_archives)
95
120
def is_ready(self):
96
"""Tell the parent process that the server is ready for writes."""
97
121
os.write(self.write_end, 'asdf')
99
123
def __enter__(self):
102
Fork and start a server in the child. Return when the server is ready
106
126
self.start_server()
111
131
def start_server(self):
112
"""Start the HTTP server."""
113
132
service = HTTPServer(('', self.port), FakeGrackleRequestHandler)
114
service.store = MemoryStore(self.message_archives)
115
for archive_id, messages in service.store.message_archives.iteritems():
133
service.store = GrackleStore(self.messages)
134
for archive_id, messages in service.store.messages.iteritems():
116
135
for message in messages:
117
136
message.setdefault('headers', {})
121
stream=sys.stderr, level=logging.INFO)
122
138
service.serve_forever()
124
140
def __exit__(self, exc_type, exc_val, traceback):
125
141
os.kill(self.pid, SIGKILL)
144
SUPPORTED_ORDERS = set(
145
['date', 'author', 'subject', 'thread_newest', 'thread_oldest',
128
149
class FakeGrackleRequestHandler(BaseHTTPRequestHandler):
129
"""A request handler that forwards to server.store."""
131
def __init__(self, *args, **kwargs):
132
"""Constructor. Sets up logging."""
133
self.logger = logging.getLogger('http')
134
BaseHTTPRequestHandler.__init__(self, *args, **kwargs)
136
151
def do_POST(self):
137
"""Create a message on POST."""
138
152
message = self.rfile.read(int(self.headers['content-length']))
139
scheme, netloc, path, params, query_string, fragments = (
141
parts = path.split('/')
142
if parts[1] == 'archive' and len(parts) == 4:
144
# This expected path is /archive/archive_id/message_id.
145
self.server.store.put_message(parts[2], parts[3], message)
146
self.send_response(httplib.CREATED)
150
self.send_error(httplib.BAD_REQUEST)
153
if message == 'This is a message':
154
self.send_response(httplib.CREATED)
158
self.send_error(httplib.BAD_REQUEST)
152
160
def do_GET(self):
153
"""Retrieve a list of messages on GET."""
154
161
scheme, netloc, path, params, query_string, fragments = (
155
162
urlparse(self.path))
156
163
parts = path.split('/')
161
168
self.send_response(httplib.OK)
162
169
self.end_headers()
163
170
self.wfile.write(simplejson.dumps(response))
164
except Exception, error:
166
httplib.BAD_REQUEST, error.__doc__)
171
except UnsupportedOrder:
172
self.send_response(httplib.BAD_REQUEST)
173
self.wfile.write('Unsupported order')
169
def log_message(self, format, *args):
170
"""Override log_message to use standard Python logging."""
171
message = "%s - - [%s] %s\n" % (
172
self.address_string(), self.log_date_time_string(), format % args)
173
self.logger.info(message)
177
def fake_grackle_service(client, messages=None):
180
return ForkedFake(client.port, messages)
176
183
class TestPutMessage(TestCase):
178
185
def test_put_message(self):
179
client = GrackleClient('localhost', 8420)
180
message_archives = {'arch1': []}
181
with ForkedFakeService.from_client(client, message_archives):
182
client.put_message('arch1', 'id1', StringIO('This is a message'))
183
response = client.get_messages('arch1')
184
self.assertEqual(1, len(response['messages']))
185
message = response['messages'][0]
186
self.assertEqual('id1', message['message_id'])
188
def test_put_message_without_archive(self):
189
client = GrackleClient('localhost', 8421)
190
message_archives = {'arch1': []}
191
with ForkedFakeService.from_client(client, message_archives):
186
client = GrackleClient('localhost', 8436)
187
with fake_grackle_service(client):
188
client.put_message('arch1', 'asdf', StringIO('This is a message'))
192
189
with ExpectedException(Exception, 'wtf'):
193
client.put_message('no-archive', 'id1', StringIO('message'))
190
client.put_message('arch1', 'asdf',
191
StringIO('This is not a message'))
196
194
class TestGetMessages(TestCase):
216
215
def test_get_messages_by_id(self):
217
216
client = GrackleClient('localhost', 8437)
219
'baz': [make_message('foo'), make_message('bar')]}
220
with ForkedFakeService.from_client(client, archive):
217
with fake_grackle_service(client,
219
[{'message_id': 'foo'},
220
{'message_id': 'bar'}]}):
221
221
response = client.get_messages('baz', message_ids=['foo'])
222
222
message, = response['messages']
223
223
self.assertEqual('foo', message['message_id'])
225
225
def test_get_messages_batching(self):
226
226
client = GrackleClient('localhost', 8438)
227
archive = {'baz': [make_message('foo'), make_message('bar')]}
228
with ForkedFakeService.from_client(client, archive):
227
with fake_grackle_service(client,
229
[{'message_id': 'foo'},
230
{'message_id': 'bar'}]}):
229
231
response = client.get_messages('baz', limit=1)
230
232
self.assertEqual(1, len(response['messages']))
231
233
messages = response['messages']
294
287
def test_get_messages_thread_newest_order(self):
295
288
client = GrackleClient('localhost', 8439)
298
make_message('bar', headers={'date': 'x'}),
299
make_message('qux', headers={'date': 'w'}),
300
make_message('foo', headers={'date': 'y',
301
'in-reply-to': 'bar'}),
302
make_message('baz', headers={'date': 'z',
303
'in-reply-to': 'qux'}),
305
with ForkedFakeService.from_client(client, archive):
289
with fake_grackle_service(client, {'baz': [
290
{'message_id': 'bar', 'date': 'x'},
291
{'message_id': 'qux', 'date': 'w'},
292
{'message_id': 'foo', 'date': 'y', 'in_reply_to': 'bar'},
293
{'message_id': 'baz', 'date': 'z', 'in_reply_to': 'qux'},
306
295
response = client.get_messages('baz', order='date')
307
296
self.assertIDOrder(
308
297
['qux', 'bar', 'foo', 'baz'], response['messages'])
313
302
def test_get_messages_unsupported_order(self):
314
303
client = GrackleClient('localhost', 8439)
317
make_message('foo', headers={'date': '2011-03-25'}),
318
make_message('foo', headers={'date': '2011-03-24'}),
320
with ForkedFakeService.from_client(client, archive):
321
with ExpectedException(UnsupportedOrder, ''):
304
with fake_grackle_service(client,
305
{'baz': [{'message_id': 'foo', 'date': '2011-03-25'},
306
{'message_id': 'bar', 'date': '2011-03-24'}]}):
307
with ExpectedException(UnsupportedOrder):
322
308
client.get_messages('baz', order='nonsense')
324
310
def test_get_messages_headers_no_headers(self):
325
311
client = GrackleClient('localhost', 8440)
326
archive = {'baz': [make_message('foo')]}
327
with ForkedFakeService.from_client(client, archive):
312
with fake_grackle_service(client,
314
{'message_id': 'foo'}
328
316
response = client.get_messages('baz', headers=[
329
317
'Subject', 'Date', 'X-Launchpad-Message-Rationale'])
330
318
first_message = response['messages'][0]
357
346
def test_get_messages_max_body_length(self):
358
347
client = GrackleClient('localhost', 8443)
359
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
360
with ForkedFakeService.from_client(client, archive):
348
with fake_grackle_service(client,
350
{'message_id': 'foo', 'body': u'abcdefghi'}
361
352
response = client.get_messages('baz', max_body_length=3)
362
353
first_message = response['messages'][0]
363
354
self.assertEqual('abc', first_message['body'])
365
356
def test_include_hidden(self):
366
357
client = GrackleClient('localhost', 8444)
369
make_message('foo', hidden=True),
370
make_message('bar', hidden=False),
372
with ForkedFakeService.from_client(client, archive):
358
with fake_grackle_service(client,
360
{'message_id': 'foo', 'hidden': True},
361
{'message_id': 'bar', 'hidden': False}
373
363
response = client.get_messages('baz', include_hidden=True)
374
364
self.assertMessageIDs(['bar', 'foo'], response['messages'])
375
365
response = client.get_messages('baz', include_hidden=False)
376
366
self.assertMessageIDs(['bar'], response['messages'])
378
def test_display_type_unknown_value(self):
379
client = GrackleClient('localhost', 8445)
380
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
381
with ForkedFakeService.from_client(client, archive):
382
with ExpectedException(UnsupportedDisplayType, ''):
383
client.get_messages('baz', display_type='unknown')
385
def test_display_type_headers_only(self):
386
client = GrackleClient('localhost', 8446)
389
make_message('foo', body=u'abcdefghi',
390
headers={'From': 'me', 'To': 'you'})]}
391
with ForkedFakeService.from_client(client, archive):
392
response = client.get_messages('baz', display_type='headers-only')
393
first_message = response['messages'][0]
394
self.assertEqual('foo', first_message['message_id'])
396
{'From': 'me', 'Message-Id': 'foo', 'To': 'you'},
397
first_message['headers'])
398
self.assertNotIn('body', first_message)
400
def test_display_type_text_only(self):
401
client = GrackleClient('localhost', 8446)
406
headers={'From': 'me', 'To': 'you'},
407
attachment_type='text/x-diff')]}
408
with ForkedFakeService.from_client(client, archive):
409
response = client.get_messages('baz', display_type='text-only')
410
first_message = response['messages'][0]
411
self.assertEqual('foo', first_message['message_id'])
412
self.assertEqual('me', first_message['headers']['From'])
413
self.assertEqual('you', first_message['headers']['To'])
414
self.assertEqual('abcdefghi', first_message['body'])
416
def test_display_type_all(self):
417
client = GrackleClient('localhost', 8447)
422
headers={'From': 'me', 'To': 'you'},
423
attachment_type='text/x-diff')]}
424
with ForkedFakeService.from_client(client, archive):
425
response = client.get_messages('baz', display_type='all')
426
first_message = response['messages'][0]
427
self.assertEqual('foo', first_message['message_id'])
428
self.assertEqual('me', first_message['headers']['From'])
429
self.assertEqual('you', first_message['headers']['To'])
431
'abcdefghi\n\nattactment data.', first_message['body'])
433
def test_date_range(self):
434
client = GrackleClient('localhost', 8448)
438
'foo', 'abcdefghi', headers={'date': '2011-12-31'}),
440
'bar', 'abcdefghi', headers={'date': '2012-01-01'}),
442
'qux', 'abcdefghi', headers={'date': '2012-01-15'}),
444
'naf', 'abcdefghi', headers={'date': '2012-01-31'}),
446
'doh', 'abcdefghi', headers={'date': '2012-02-01'}),
448
with ForkedFakeService.from_client(client, archive):
449
response = client.get_messages(
450
'baz', date_range='2012-01-01..2012-01-31')
451
ids = sorted(m['message_id'] for m in response['messages'])
452
self.assertEqual(['bar', 'naf', 'qux'], ids)
454
def test_date_range_unparsabledaterange(self):
455
client = GrackleClient('localhost', 8449)
456
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
457
with ForkedFakeService.from_client(client, archive):
458
with ExpectedException(UnparsableDateRange, ''):
459
client.get_messages('baz', date_range='2012-01-01')
461
def test_date_range_unparsabledaterange_missing_part(self):
462
client = GrackleClient('localhost', 8450)
463
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
464
with ForkedFakeService.from_client(client, archive):
465
with ExpectedException(UnparsableDateRange, ''):
466
client.get_messages('baz', date_range='2012-01-01..')
468
def test_date_range_unparsabledaterange_extra_part(self):
469
client = GrackleClient('localhost', 8451)
470
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
471
with ForkedFakeService.from_client(client, archive):
472
with ExpectedException(UnparsableDateRange, ''):
473
client.get_messages('baz', date_range='2012-01..12-02..12-03')