3
3
BaseHTTPRequestHandler,
5
from email.message import Message
6
from email.mime.multipart import MIMEMultipart
7
from email.mime.text import MIMEText
7
11
from signal import SIGKILL
9
13
from StringIO import StringIO
10
15
from unittest import TestCase
11
16
from urlparse import urlparse
12
from urlparse import parse_qs
14
18
from testtools import ExpectedException
16
20
from grackle.client import (
23
UnsupportedDisplayType,
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])
63
messages = [m for m in messages
66
for message in messages:
68
not parameters['include_hidden']
69
and message.get('hidden', False)):
72
if ('message_ids' in parameters and
73
message['message_id'] not in parameters['message_ids']):
75
message = dict(message)
76
if 'headers' in parameters:
78
(k, v) for k, v in message['headers'].iteritems()
79
if k in parameters['headers'])
80
message['headers'] = headers
81
max_body = parameters.get('max_body_length')
82
if max_body is not None:
83
message['body'] = message['body'][:max_body]
84
new_messages.append(message)
85
messages = new_messages
86
limit = parameters.get('limit', 100)
87
memo = parameters.get('memo')
88
message_id_indices = dict(
89
(m['message_id'], idx) for idx, m in enumerate(messages))
93
start = message_id_indices[memo.encode('rot13')]
95
previous_memo = messages[start - 1]['message_id'].encode('rot13')
98
end = min(start + limit, len(messages))
99
if end < len(messages):
100
next_memo = messages[end]['message_id'].encode('rot13')
103
messages = messages[start:end]
106
'messages': messages,
107
'next_memo': next_memo,
108
'previous_memo': previous_memo
116
def __init__(self, port, messages=None):
26
from grackle.store import (
32
def make_message(message_id, body='body', headers=None, hidden=False):
36
'Message-Id': message_id,
42
message_headers.update(headers.items())
44
message.set_payload(body)
45
for key, value in message_headers.items():
47
return make_json_message(message_id, message.as_string(), hidden)
50
def make_mime_message(message_id, body='body', headers=None, hidden=False,
51
attachment_type=None):
52
parts = MIMEMultipart()
53
parts.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
parts.attach(attachment)
60
return make_message(message_id, parts.as_string(), 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.
119
self.messages = messages
77
if message_archives is None:
78
self.message_archives = {}
80
self.message_archives = message_archives
120
81
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)
122
95
def is_ready(self):
96
"""Tell the parent process that the server is ready for writes."""
123
97
os.write(self.write_end, 'asdf')
125
99
def __enter__(self):
102
Fork and start a server in the child. Return when the server is ready
128
106
self.start_server()
133
111
def start_server(self):
112
"""Start the HTTP server."""
134
113
service = HTTPServer(('', self.port), FakeGrackleRequestHandler)
135
service.store = GrackleStore(self.messages)
136
for archive_id, messages in service.store.messages.iteritems():
114
service.store = MemoryStore(self.message_archives)
115
for archive_id, messages in service.store.message_archives.iteritems():
137
116
for message in messages:
138
117
message.setdefault('headers', {})
121
stream=sys.stderr, level=logging.INFO)
140
122
service.serve_forever()
142
124
def __exit__(self, exc_type, exc_val, traceback):
143
125
os.kill(self.pid, SIGKILL)
146
SUPPORTED_ORDERS = set(
147
['date', 'author', 'subject', 'thread_newest', 'thread_oldest',
151
128
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)
153
136
def do_POST(self):
154
message = self.rfile.read(int(self.headers['content-length']))
155
if message == 'This is a message':
156
self.send_response(httplib.CREATED)
137
"""Create a message on POST."""
138
scheme, netloc, path, params, query_string, fragments = (
140
parts = path.split('/')
141
if parts[1] != 'archive':
142
# This is an unknonwn operation?
144
if 'content-length' in self.headers:
145
operation = 'put_message'
160
self.send_error(httplib.BAD_REQUEST)
147
operation = 'hide_message'
148
if operation == 'put_message':
149
message = self.rfile.read(int(self.headers['content-length']))
151
# This expected path is /archive/archive_id/message_id.
152
self.server.store.put_message(parts[2], parts[3], message)
153
self.send_response(httplib.CREATED)
157
self.send_error(httplib.BAD_REQUEST)
158
elif operation == 'hide_message':
160
# This expected path is /archive/archive_id/message_id.
161
response = self.server.store.hide_message(
162
parts[2], parts[3], query_string)
163
self.send_response(httplib.OK)
165
self.wfile.write(simplejson.dumps(response))
167
self.send_error(httplib.BAD_REQUEST)
162
169
def do_GET(self):
170
"""Retrieve a list of messages on GET."""
163
171
scheme, netloc, path, params, query_string, fragments = (
164
172
urlparse(self.path))
165
173
parts = path.split('/')
170
178
self.send_response(httplib.OK)
171
179
self.end_headers()
172
180
self.wfile.write(simplejson.dumps(response))
173
except UnsupportedOrder:
174
self.send_response(httplib.BAD_REQUEST)
175
self.wfile.write('Unsupported order')
181
except Exception, error:
183
httplib.BAD_REQUEST, error.__doc__)
179
def fake_grackle_service(client, messages=None):
182
return ForkedFake(client.port, messages)
186
def log_message(self, format, *args):
187
"""Override log_message to use standard Python logging."""
188
message = "%s - - [%s] %s\n" % (
189
self.address_string(), self.log_date_time_string(), format % args)
190
self.logger.info(message)
185
193
class TestPutMessage(TestCase):
187
195
def test_put_message(self):
188
client = GrackleClient('localhost', 8436)
189
with fake_grackle_service(client):
190
client.put_message('arch1', 'asdf', StringIO('This is a message'))
196
client = GrackleClient('localhost', 8420)
197
message_archives = {'arch1': []}
198
with ForkedFakeService.from_client(client, message_archives):
199
client.put_message('arch1', 'id1', StringIO('This is a message'))
200
response = client.get_messages('arch1')
201
self.assertEqual(1, len(response['messages']))
202
message = response['messages'][0]
203
self.assertEqual('id1', message['message_id'])
205
def test_put_message_without_archive(self):
206
client = GrackleClient('localhost', 8421)
207
message_archives = {'arch1': []}
208
with ForkedFakeService.from_client(client, message_archives):
191
209
with ExpectedException(Exception, 'wtf'):
192
client.put_message('arch1', 'asdf',
193
StringIO('This is not a message'))
210
client.put_message('no-archive', 'id1', StringIO('message'))
196
213
class TestGetMessages(TestCase):
304
334
def test_get_messages_unsupported_order(self):
305
335
client = GrackleClient('localhost', 8439)
306
with fake_grackle_service(client,
307
{'baz': [{'message_id': 'foo', 'date': '2011-03-25'},
308
{'message_id': 'bar', 'date': '2011-03-24'}]}):
309
with ExpectedException(UnsupportedOrder):
338
make_message('foo', headers={'date': '2011-03-25'}),
339
make_message('foo', headers={'date': '2011-03-24'}),
341
with ForkedFakeService.from_client(client, archive):
342
with ExpectedException(UnsupportedOrder, ''):
310
343
client.get_messages('baz', order='nonsense')
312
345
def test_get_messages_headers_no_headers(self):
313
346
client = GrackleClient('localhost', 8440)
314
with fake_grackle_service(client,
316
{'message_id': 'foo'}
347
archive = {'baz': [make_message('foo')]}
348
with ForkedFakeService.from_client(client, archive):
318
349
response = client.get_messages('baz', headers=[
319
350
'Subject', 'Date', 'X-Launchpad-Message-Rationale'])
320
351
first_message = response['messages'][0]
348
378
def test_get_messages_max_body_length(self):
349
379
client = GrackleClient('localhost', 8443)
350
with fake_grackle_service(client,
352
{'message_id': 'foo', 'body': u'abcdefghi'}
380
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
381
with ForkedFakeService.from_client(client, archive):
354
382
response = client.get_messages('baz', max_body_length=3)
355
383
first_message = response['messages'][0]
356
384
self.assertEqual('abc', first_message['body'])
358
386
def test_include_hidden(self):
359
387
client = GrackleClient('localhost', 8444)
360
with fake_grackle_service(client,
362
{'message_id': 'foo', 'hidden': True},
363
{'message_id': 'bar', 'hidden': False}
390
make_message('foo', hidden=True),
391
make_message('bar', hidden=False),
393
with ForkedFakeService.from_client(client, archive):
365
394
response = client.get_messages('baz', include_hidden=True)
366
395
self.assertMessageIDs(['bar', 'foo'], response['messages'])
367
396
response = client.get_messages('baz', include_hidden=False)
368
397
self.assertMessageIDs(['bar'], response['messages'])
399
def test_display_type_unknown_value(self):
400
client = GrackleClient('localhost', 8445)
401
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
402
with ForkedFakeService.from_client(client, archive):
403
with ExpectedException(UnsupportedDisplayType, ''):
404
client.get_messages('baz', display_type='unknown')
406
def test_display_type_headers_only(self):
407
client = GrackleClient('localhost', 8446)
410
make_message('foo', body=u'abcdefghi',
411
headers={'From': 'me', 'To': 'you'})]}
412
with ForkedFakeService.from_client(client, archive):
413
response = client.get_messages('baz', display_type='headers-only')
414
first_message = response['messages'][0]
415
self.assertEqual('foo', first_message['message_id'])
417
archive['baz'][0]['headers'], first_message['headers'])
418
self.assertNotIn('body', first_message)
420
def test_display_type_text_only(self):
421
client = GrackleClient('localhost', 8446)
426
headers={'From': 'me', 'To': 'you'},
427
attachment_type='text/x-diff')]}
428
with ForkedFakeService.from_client(client, archive):
429
response = client.get_messages('baz', display_type='text-only')
430
first_message = response['messages'][0]
431
self.assertEqual('foo', first_message['message_id'])
432
self.assertEqual('me', first_message['headers']['From'])
433
self.assertEqual('you', first_message['headers']['To'])
434
self.assertEqual(archive['baz'][0]['body'], first_message['body'])
436
def test_display_type_all(self):
437
client = GrackleClient('localhost', 8447)
442
headers={'From': 'me', 'To': 'you'},
443
attachment_type='text/x-diff')]}
444
with ForkedFakeService.from_client(client, archive):
445
response = client.get_messages('baz', display_type='all')
446
first_message = response['messages'][0]
447
self.assertEqual('foo', first_message['message_id'])
448
self.assertEqual('me', first_message['headers']['From'])
449
self.assertEqual('you', first_message['headers']['To'])
450
self.assertEqual(archive['baz'][0]['body'], first_message['body'])
452
def test_date_range(self):
453
client = GrackleClient('localhost', 8448)
457
'foo', 'abcdefghi', headers={'date': '2011-12-31'}),
459
'bar', 'abcdefghi', headers={'date': '2012-01-01'}),
461
'qux', 'abcdefghi', headers={'date': '2012-01-15'}),
463
'naf', 'abcdefghi', headers={'date': '2012-01-31'}),
465
'doh', 'abcdefghi', headers={'date': '2012-02-01'}),
467
with ForkedFakeService.from_client(client, archive):
468
response = client.get_messages(
469
'baz', date_range='2012-01-01..2012-01-31')
470
ids = sorted(m['message_id'] for m in response['messages'])
471
self.assertEqual(['bar', 'naf', 'qux'], ids)
473
def test_date_range_unparsabledaterange(self):
474
client = GrackleClient('localhost', 8449)
475
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
476
with ForkedFakeService.from_client(client, archive):
477
with ExpectedException(UnparsableDateRange, ''):
478
client.get_messages('baz', date_range='2012-01-01')
480
def test_date_range_unparsabledaterange_missing_part(self):
481
client = GrackleClient('localhost', 8450)
482
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
483
with ForkedFakeService.from_client(client, archive):
484
with ExpectedException(UnparsableDateRange, ''):
485
client.get_messages('baz', date_range='2012-01-01..')
487
def test_date_range_unparsabledaterange_extra_part(self):
488
client = GrackleClient('localhost', 8451)
489
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
490
with ForkedFakeService.from_client(client, archive):
491
with ExpectedException(UnparsableDateRange, ''):
492
client.get_messages('baz', date_range='2012-01..12-02..12-03')
495
class TestHideMessages(TestCase):
497
def test_hide_message_true(self):
498
client = GrackleClient('localhost', 8470)
501
make_message('foo', hidden=False),
503
with ForkedFakeService.from_client(client, archive):
504
response = client.hide_message('baz', 'foo', hidden=True)
505
self.assertEqual('foo', response['message_id'])
506
self.assertIs(True, response['hidden'])
508
def test_hide_message_false(self):
509
client = GrackleClient('localhost', 8470)
512
make_message('foo', hidden=True),
514
with ForkedFakeService.from_client(client, archive):
515
response = client.hide_message('baz', 'foo', hidden=False)
516
self.assertEqual('foo', response['message_id'])
517
self.assertIs(False, response['hidden'])