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):
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.
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, port, messages=None):
77
if message_archives is None:
78
self.message_archives = {}
80
self.message_archives = message_archives
42
self.messages = messages
81
43
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
45
def is_ready(self):
96
"""Tell the parent process that the server is ready for writes."""
97
46
os.write(self.write_end, 'asdf')
99
48
def __enter__(self):
102
Fork and start a server in the child. Return when the server is ready
106
51
self.start_server()
111
56
def start_server(self):
112
"""Start the HTTP server."""
113
57
service = HTTPServer(('', self.port), FakeGrackleRequestHandler)
114
service.store = MemoryStore(self.message_archives)
115
for archive_id, messages in service.store.message_archives.iteritems():
116
for message in messages:
117
message.setdefault('headers', {})
58
service.messages = self.messages
121
stream=sys.stderr, level=logging.INFO)
122
60
service.serve_forever()
124
62
def __exit__(self, exc_type, exc_val, traceback):
125
63
os.kill(self.pid, SIGKILL)
66
SUPPORTED_ORDERS = set(
67
['date', 'author', 'subject', 'thread_newest', 'thread_oldest',
128
71
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
73
def do_POST(self):
137
"""Create a message on POST."""
138
74
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)
75
if message == 'This is a message':
76
self.send_response(httplib.CREATED)
80
self.send_error(httplib.BAD_REQUEST)
153
"""Retrieve a list of messages on GET."""
154
83
scheme, netloc, path, params, query_string, fragments = (
155
84
urlparse(self.path))
156
parts = path.split('/')
157
if parts[1] == 'archive':
159
response = self.server.store.get_messages(
160
parts[2], query_string)
161
self.send_response(httplib.OK)
163
self.wfile.write(simplejson.dumps(response))
164
except Exception, error:
166
httplib.BAD_REQUEST, error.__doc__)
85
archive = os.path.split(path)[1]
86
query = parse_qs(query_string)
87
parameters = simplejson.loads(query['parameters'][0])
88
order = parameters.get('order')
89
messages = self.server.messages[archive]
90
if order is not None :
91
if order not in SUPPORTED_ORDERS:
92
self.send_response(httplib.BAD_REQUEST)
93
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)
95
elif order.startswith('thread_'):
96
threaded = threaded_messages(messages)
98
if order == 'thread_subject':
99
threaded.sort(key=lambda t: t[0]['subject'])
100
if order == 'thread_oldest':
101
threaded.sort(key=lambda t: min(m['date'] for m in t))
102
if order == 'thread_newest':
103
threaded.sort(key=lambda t: max(m['date'] for m in t))
104
for thread in threaded:
105
messages.extend(thread)
107
messages.sort(key=lambda m: m[order])
108
messages = [m for m in messages
109
if 'message_ids' not in parameters or
110
m['message_id'] in parameters['message_ids']]
111
self.send_response(httplib.OK)
113
limit = parameters.get('limit', 100)
114
memo = parameters.get('memo')
115
message_id_indices = dict(
116
(m['message_id'], idx) for idx, m in enumerate(messages))
120
start = message_id_indices[memo.encode('rot13')]
122
previous_memo = messages[start - 1]['message_id'].encode('rot13')
125
end = min(start + limit, len(messages))
126
if end < len(messages):
127
next_memo = messages[end]['message_id'].encode('rot13')
130
messages = messages[start:end]
132
'messages': messages,
133
'next_memo': next_memo,
134
'previous_memo': previous_memo
136
self.wfile.write(simplejson.dumps(response))
139
def fake_grackle_service(client, messages=None):
142
return ForkedFake(client.port, messages)
176
145
class TestPutMessage(TestCase):
178
147
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):
148
client = GrackleClient('localhost', 8436)
149
with fake_grackle_service(client):
150
client.put_message('arch1', 'asdf', StringIO('This is a message'))
192
151
with ExpectedException(Exception, 'wtf'):
193
client.put_message('no-archive', 'id1', StringIO('message'))
152
client.put_message('arch1', 'asdf',
153
StringIO('This is not a message'))
196
156
class TestGetMessages(TestCase):
317
264
def test_get_messages_unsupported_order(self):
318
265
client = GrackleClient('localhost', 8439)
321
make_message('foo', headers={'date': '2011-03-25'}),
322
make_message('foo', headers={'date': '2011-03-24'}),
324
with ForkedFakeService.from_client(client, archive):
325
with ExpectedException(UnsupportedOrder, ''):
266
with fake_grackle_service(client,
267
{'baz': [{'message_id': 'foo', 'date': '2011-03-25'},
268
{'message_id': 'bar', 'date': '2011-03-24'}]}):
269
with ExpectedException(UnsupportedOrder):
326
270
client.get_messages('baz', order='nonsense')
328
def test_get_messages_headers_no_headers(self):
329
client = GrackleClient('localhost', 8440)
330
archive = {'baz': [make_message('foo')]}
331
with ForkedFakeService.from_client(client, archive):
332
response = client.get_messages('baz', headers=[
333
'Subject', 'Date', 'X-Launchpad-Message-Rationale'])
334
first_message = response['messages'][0]
335
self.assertEqual('foo', first_message['message_id'])
336
self.assertEqual({}, first_message['headers'])
338
def test_get_messages_headers_exclude_headers(self):
339
client = GrackleClient('localhost', 8441)
341
'baz': [make_message('foo', headers={'From': 'me'})]}
342
with ForkedFakeService.from_client(client, archive):
343
response = client.get_messages('baz', headers=[
344
'Subject', 'Date', 'X-Launchpad-Message-Rationale'])
345
first_message = response['messages'][0]
346
self.assertEqual('foo', first_message['message_id'])
347
self.assertEqual({}, first_message['headers'])
349
def test_get_messages_headers_include_headers(self):
350
client = GrackleClient('localhost', 8442)
353
make_message('foo', headers={'From': 'me', 'To': 'you'})]}
354
with ForkedFakeService.from_client(client, archive):
355
response = client.get_messages('baz', headers=[
357
first_message = response['messages'][0]
358
self.assertEqual('foo', first_message['message_id'])
359
self.assertEqual({'From': 'me', 'To': 'you'}, first_message['headers'])
361
def test_get_messages_max_body_length(self):
362
client = GrackleClient('localhost', 8443)
363
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
364
with ForkedFakeService.from_client(client, archive):
365
response = client.get_messages('baz', max_body_length=3)
366
first_message = response['messages'][0]
367
self.assertEqual('abc', first_message['body'])
369
def test_include_hidden(self):
370
client = GrackleClient('localhost', 8444)
373
make_message('foo', hidden=True),
374
make_message('bar', hidden=False),
376
with ForkedFakeService.from_client(client, archive):
377
response = client.get_messages('baz', include_hidden=True)
378
self.assertMessageIDs(['bar', 'foo'], response['messages'])
379
response = client.get_messages('baz', include_hidden=False)
380
self.assertMessageIDs(['bar'], response['messages'])
382
def test_display_type_unknown_value(self):
383
client = GrackleClient('localhost', 8445)
384
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
385
with ForkedFakeService.from_client(client, archive):
386
with ExpectedException(UnsupportedDisplayType, ''):
387
client.get_messages('baz', display_type='unknown')
389
def test_display_type_headers_only(self):
390
client = GrackleClient('localhost', 8446)
393
make_message('foo', body=u'abcdefghi',
394
headers={'From': 'me', 'To': 'you'})]}
395
with ForkedFakeService.from_client(client, archive):
396
response = client.get_messages('baz', display_type='headers-only')
397
first_message = response['messages'][0]
398
self.assertEqual('foo', first_message['message_id'])
400
archive['baz'][0]['headers'], first_message['headers'])
401
self.assertNotIn('body', first_message)
403
def test_display_type_text_only(self):
404
client = GrackleClient('localhost', 8446)
409
headers={'From': 'me', 'To': 'you'},
410
attachment_type='text/x-diff')]}
411
with ForkedFakeService.from_client(client, archive):
412
response = client.get_messages('baz', display_type='text-only')
413
first_message = response['messages'][0]
414
self.assertEqual('foo', first_message['message_id'])
415
self.assertEqual('me', first_message['headers']['From'])
416
self.assertEqual('you', first_message['headers']['To'])
417
self.assertEqual(archive['baz'][0]['body'], first_message['body'])
419
def test_display_type_all(self):
420
client = GrackleClient('localhost', 8447)
425
headers={'From': 'me', 'To': 'you'},
426
attachment_type='text/x-diff')]}
427
with ForkedFakeService.from_client(client, archive):
428
response = client.get_messages('baz', display_type='all')
429
first_message = response['messages'][0]
430
self.assertEqual('foo', first_message['message_id'])
431
self.assertEqual('me', first_message['headers']['From'])
432
self.assertEqual('you', first_message['headers']['To'])
433
self.assertEqual(archive['baz'][0]['body'], first_message['body'])
435
def test_date_range(self):
436
client = GrackleClient('localhost', 8448)
440
'foo', 'abcdefghi', headers={'date': '2011-12-31'}),
442
'bar', 'abcdefghi', headers={'date': '2012-01-01'}),
444
'qux', 'abcdefghi', headers={'date': '2012-01-15'}),
446
'naf', 'abcdefghi', headers={'date': '2012-01-31'}),
448
'doh', 'abcdefghi', headers={'date': '2012-02-01'}),
450
with ForkedFakeService.from_client(client, archive):
451
response = client.get_messages(
452
'baz', date_range='2012-01-01..2012-01-31')
453
ids = sorted(m['message_id'] for m in response['messages'])
454
self.assertEqual(['bar', 'naf', 'qux'], ids)
456
def test_date_range_unparsabledaterange(self):
457
client = GrackleClient('localhost', 8449)
458
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
459
with ForkedFakeService.from_client(client, archive):
460
with ExpectedException(UnparsableDateRange, ''):
461
client.get_messages('baz', date_range='2012-01-01')
463
def test_date_range_unparsabledaterange_missing_part(self):
464
client = GrackleClient('localhost', 8450)
465
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
466
with ForkedFakeService.from_client(client, archive):
467
with ExpectedException(UnparsableDateRange, ''):
468
client.get_messages('baz', date_range='2012-01-01..')
470
def test_date_range_unparsabledaterange_extra_part(self):
471
client = GrackleClient('localhost', 8451)
472
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
473
with ForkedFakeService.from_client(client, archive):
474
with ExpectedException(UnparsableDateRange, ''):
475
client.get_messages('baz', date_range='2012-01..12-02..12-03')