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):
55
parts = MIMEMultipart()
56
parts.attach(MIMEText(body))
57
if attachment_type is not None:
58
attachment = Message()
59
attachment.set_payload('attactment data.')
60
attachment['Content-Type'] = attachment_type
61
attachment['Content-Disposition'] = 'attachment; filename="file.ext"'
62
parts.attach(attachment)
64
message.set_payload(parts.as_string())
65
for key, value in headers.items():
67
return make_json_message(message_id, message.as_string())
70
class ForkedFakeService:
71
"""A Grackle service fake, as a ContextManager."""
73
def __init__(self, port, message_archives=None, write_logs=False):
76
:param port: The tcp port to use.
77
:param message_archives: A dict of lists of dicts representing
78
archives of messages. The outer dict represents the archive,
79
the list represents the list of messages for that archive.
80
:param write_logs: If true, log messages will be written to stdout.
23
def __init__(self, port, messages=None):
84
if message_archives is None:
85
self.message_archives = {}
87
self.message_archives = message_archives
26
self.messages = messages
88
27
self.read_end, self.write_end = os.pipe()
89
self.write_logs = write_logs
92
def from_client(client, message_archives=None):
93
"""Instantiate a ForkedFakeService from the client.
95
:param port: The client to provide service for.
96
:param message_archives: A dict of lists of dicts representing
97
archives of messages. The outer dict represents the archive,
98
the list represents the list of messages for that archive.
100
return ForkedFakeService(client.port, message_archives)
102
29
def is_ready(self):
103
"""Tell the parent process that the server is ready for writes."""
104
30
os.write(self.write_end, 'asdf')
106
32
def __enter__(self):
109
Fork and start a server in the child. Return when the server is ready
113
35
self.start_server()
135
50
class FakeGrackleRequestHandler(BaseHTTPRequestHandler):
136
"""A request handler that forwards to server.store."""
138
def __init__(self, *args, **kwargs):
139
"""Constructor. Sets up logging."""
140
self.logger = logging.getLogger('http')
141
BaseHTTPRequestHandler.__init__(self, *args, **kwargs)
143
52
def do_POST(self):
144
"""Create a message on POST."""
145
53
message = self.rfile.read(int(self.headers['content-length']))
146
scheme, netloc, path, params, query_string, fragments = (
148
parts = path.split('/')
149
if parts[1] == 'archive' and len(parts) == 4:
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)
54
if message == 'This is a message':
55
self.send_response(httplib.CREATED)
59
self.send_error(httplib.BAD_REQUEST)
160
"""Retrieve a list of messages on GET."""
161
62
scheme, netloc, path, params, query_string, fragments = (
162
63
urlparse(self.path))
163
parts = path.split('/')
164
if parts[1] == 'archive':
166
response = self.server.store.get_messages(
167
parts[2], query_string)
168
self.send_response(httplib.OK)
170
self.wfile.write(simplejson.dumps(response))
171
except Exception, error:
173
httplib.BAD_REQUEST, error.__doc__)
176
def log_message(self, format, *args):
177
"""Override log_message to use standard Python logging."""
178
message = "%s - - [%s] %s\n" % (
179
self.address_string(), self.log_date_time_string(), format % args)
180
self.logger.info(message)
64
archive = os.path.split(path)[1]
65
query = parse_qs(query_string)
66
parameters = simplejson.loads(query['parameters'][0])
67
self.send_response(httplib.OK)
69
messages = [m for m in self.server.messages[archive] if 'message_ids'
70
not in parameters or m['message_id'] in
71
parameters['message_ids']]
72
limit = parameters.get('limit', 100)
73
memo = parameters.get('memo')
74
message_id_indices = dict(
75
(m['message_id'], idx) for idx, m in enumerate(messages))
79
start = message_id_indices[memo.encode('rot13')]
81
previous_memo = messages[start - 1]['message_id'].encode('rot13')
84
end = min(start + limit, len(messages))
85
if end < len(messages):
86
next_memo = messages[end]['message_id'].encode('rot13')
89
messages = messages[start:end]
92
'next_memo': next_memo,
93
'previous_memo': previous_memo
95
self.wfile.write(simplejson.dumps(response))
98
def fake_grackle_service(client, messages=None):
101
return ForkedFake(client.port, messages)
183
104
class TestPutMessage(TestCase):
185
106
def test_put_message(self):
186
client = GrackleClient('localhost', 8420)
187
message_archives = {'arch1': []}
188
with ForkedFakeService.from_client(client, message_archives):
189
client.put_message('arch1', 'id1', StringIO('This is a message'))
190
response = client.get_messages('arch1')
191
self.assertEqual(1, len(response['messages']))
192
message = response['messages'][0]
193
self.assertEqual('id1', message['message_id'])
195
def test_put_message_without_archive(self):
196
client = GrackleClient('localhost', 8421)
197
message_archives = {'arch1': []}
198
with ForkedFakeService.from_client(client, message_archives):
107
client = GrackleClient('localhost', 8436)
108
with fake_grackle_service(client):
109
client.put_message('arch1', 'asdf', StringIO('This is a message'))
199
110
with ExpectedException(Exception, 'wtf'):
200
client.put_message('no-archive', 'id1', StringIO('message'))
111
client.put_message('arch1', 'asdf',
112
StringIO('This is not a message'))
203
115
class TestGetMessages(TestCase):
205
def assertIDOrder(self, ids, messages):
206
self.assertEqual(ids, [m['message_id'] for m in messages])
208
117
def assertMessageIDs(self, ids, messages):
210
sorted(ids), sorted(messages, key=lambda m: m['message_id']))
119
sorted(ids), sorted(m['message_id'] for m in messages))
212
121
def test_get_messages(self):
213
client = GrackleClient('localhost', 8430)
215
'baz': [make_message('foo'), make_message('bar')]}
216
with ForkedFakeService.from_client(client, archive):
122
client = GrackleClient('localhost', 8435)
123
with fake_grackle_service(client,
125
[{'message_id': 'foo'},
126
{'message_id': 'bar'}]}):
217
127
response = client.get_messages('baz')
218
128
self.assertEqual(['bar', 'foo'], sorted(m['message_id'] for m in
219
129
response['messages']))
241
154
self.assertEqual(1, len(response['messages']))
242
155
messages.extend(response['messages'])
243
156
self.assertMessageIDs(['foo', 'bar'], messages)
245
def get_messages_member_order_test(self, key):
246
client = GrackleClient('localhost', 8439)
249
make_message('foo', headers={key: '2011-03-25'}),
250
make_message('bar', headers={key: '2011-03-24'}),
252
with ForkedFakeService.from_client(client, archive):
253
response = client.get_messages('baz')
254
self.assertIDOrder(['foo', 'bar'], response['messages'])
255
response = client.get_messages('baz', order=key)
256
self.assertIDOrder(['bar', 'foo'], response['messages'])
258
def test_get_messages_date_order(self):
259
self.get_messages_member_order_test('date')
261
def test_get_messages_author_order(self):
262
self.get_messages_member_order_test('author')
264
def test_get_messages_subject_order(self):
265
self.get_messages_member_order_test('subject')
267
def test_get_messages_thread_subject_order(self):
270
make_message('bar', headers={'subject': 'y'}),
271
make_message('qux', headers={'subject': 'z'}),
272
make_message('foo', headers={'subject': 'x',
273
'in-reply-to': 'qux'}),
275
client = GrackleClient('localhost', 8439)
276
with ForkedFakeService.from_client(client, archive):
277
response = client.get_messages('baz')
278
self.assertIDOrder(['bar', 'qux', 'foo'], response['messages'])
279
response = client.get_messages('baz', order='subject')
280
self.assertIDOrder(['foo', 'bar', 'qux'], response['messages'])
281
response = client.get_messages('baz', order='thread_subject')
282
self.assertIDOrder(['bar', 'qux', 'foo'], response['messages'])
284
def test_get_messages_thread_oldest_order(self):
285
client = GrackleClient('localhost', 8439)
288
make_message('bar', headers={'date': 'x'}),
289
make_message('qux', headers={'date': 'z'}),
290
make_message('foo', headers={'date': 'y',
291
'in-reply-to': 'qux'}),
293
with ForkedFakeService.from_client(client, archive):
294
response = client.get_messages('baz')
295
self.assertIDOrder(['bar', 'qux', 'foo'], response['messages'])
296
response = client.get_messages('baz', order='date')
297
self.assertIDOrder(['bar', 'foo', 'qux'], response['messages'])
298
response = client.get_messages('baz', order='thread_oldest')
299
self.assertIDOrder(['bar', 'qux', 'foo'], response['messages'])
301
def test_get_messages_thread_newest_order(self):
302
client = GrackleClient('localhost', 8439)
305
make_message('bar', headers={'date': 'x'}),
306
make_message('qux', headers={'date': 'w'}),
307
make_message('foo', headers={'date': 'y',
308
'in-reply-to': 'bar'}),
309
make_message('baz', headers={'date': 'z',
310
'in-reply-to': 'qux'}),
312
with ForkedFakeService.from_client(client, archive):
313
response = client.get_messages('baz', order='date')
315
['qux', 'bar', 'foo', 'baz'], response['messages'])
316
response = client.get_messages('baz', order='thread_newest')
318
['bar', 'foo', 'qux', 'baz'], response['messages'])
320
def test_get_messages_unsupported_order(self):
321
client = GrackleClient('localhost', 8439)
324
make_message('foo', headers={'date': '2011-03-25'}),
325
make_message('foo', headers={'date': '2011-03-24'}),
327
with ForkedFakeService.from_client(client, archive):
328
with ExpectedException(UnsupportedOrder, ''):
329
client.get_messages('baz', order='nonsense')
331
def test_get_messages_headers_no_headers(self):
332
client = GrackleClient('localhost', 8440)
333
archive = {'baz': [make_message('foo')]}
334
with ForkedFakeService.from_client(client, archive):
335
response = client.get_messages('baz', headers=[
336
'Subject', 'Date', 'X-Launchpad-Message-Rationale'])
337
first_message = response['messages'][0]
338
self.assertEqual('foo', first_message['message_id'])
339
self.assertEqual({}, first_message['headers'])
341
def test_get_messages_headers_exclude_headers(self):
342
client = GrackleClient('localhost', 8441)
344
'baz': [make_message('foo', headers={'From': 'me'})]}
345
with ForkedFakeService.from_client(client, archive):
346
response = client.get_messages('baz', headers=[
347
'Subject', 'Date', 'X-Launchpad-Message-Rationale'])
348
first_message = response['messages'][0]
349
self.assertEqual('foo', first_message['message_id'])
350
self.assertEqual({}, first_message['headers'])
352
def test_get_messages_headers_include_headers(self):
353
client = GrackleClient('localhost', 8442)
356
make_message('foo', headers={'From': 'me', 'To': 'you'})]}
357
with ForkedFakeService.from_client(client, archive):
358
response = client.get_messages('baz', headers=[
360
first_message = response['messages'][0]
361
self.assertEqual('foo', first_message['message_id'])
362
self.assertEqual({'From': 'me', 'To': 'you'}, first_message['headers'])
364
def test_get_messages_max_body_length(self):
365
client = GrackleClient('localhost', 8443)
366
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
367
with ForkedFakeService.from_client(client, archive):
368
response = client.get_messages('baz', max_body_length=3)
369
first_message = response['messages'][0]
370
self.assertEqual('abc', first_message['body'])
372
def test_include_hidden(self):
373
client = GrackleClient('localhost', 8444)
376
make_message('foo', hidden=True),
377
make_message('bar', hidden=False),
379
with ForkedFakeService.from_client(client, archive):
380
response = client.get_messages('baz', include_hidden=True)
381
self.assertMessageIDs(['bar', 'foo'], response['messages'])
382
response = client.get_messages('baz', include_hidden=False)
383
self.assertMessageIDs(['bar'], response['messages'])
385
def test_display_type_unknown_value(self):
386
client = GrackleClient('localhost', 8445)
387
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
388
with ForkedFakeService.from_client(client, archive):
389
with ExpectedException(UnsupportedDisplayType, ''):
390
client.get_messages('baz', display_type='unknown')
392
def test_display_type_headers_only(self):
393
client = GrackleClient('localhost', 8446)
396
make_message('foo', body=u'abcdefghi',
397
headers={'From': 'me', 'To': 'you'})]}
398
with ForkedFakeService.from_client(client, archive):
399
response = client.get_messages('baz', display_type='headers-only')
400
first_message = response['messages'][0]
401
self.assertEqual('foo', first_message['message_id'])
403
{'From': 'me', 'Message-Id': 'foo', 'To': 'you'},
404
first_message['headers'])
405
self.assertNotIn('body', first_message)
407
def test_display_type_text_only(self):
408
client = GrackleClient('localhost', 8446)
413
headers={'From': 'me', 'To': 'you'},
414
attachment_type='text/x-diff')]}
415
with ForkedFakeService.from_client(client, archive):
416
response = client.get_messages('baz', display_type='text-only')
417
first_message = response['messages'][0]
418
self.assertEqual('foo', first_message['message_id'])
419
self.assertEqual('me', first_message['headers']['From'])
420
self.assertEqual('you', first_message['headers']['To'])
421
self.assertEqual(archive['baz'][0]['body'], first_message['body'])
423
def test_display_type_all(self):
424
client = GrackleClient('localhost', 8447)
429
headers={'From': 'me', 'To': 'you'},
430
attachment_type='text/x-diff')]}
431
with ForkedFakeService.from_client(client, archive):
432
response = client.get_messages('baz', display_type='all')
433
first_message = response['messages'][0]
434
self.assertEqual('foo', first_message['message_id'])
435
self.assertEqual('me', first_message['headers']['From'])
436
self.assertEqual('you', first_message['headers']['To'])
437
self.assertEqual(archive['baz'][0]['body'], first_message['body'])
439
def test_date_range(self):
440
client = GrackleClient('localhost', 8448)
444
'foo', 'abcdefghi', headers={'date': '2011-12-31'}),
446
'bar', 'abcdefghi', headers={'date': '2012-01-01'}),
448
'qux', 'abcdefghi', headers={'date': '2012-01-15'}),
450
'naf', 'abcdefghi', headers={'date': '2012-01-31'}),
452
'doh', 'abcdefghi', headers={'date': '2012-02-01'}),
454
with ForkedFakeService.from_client(client, archive):
455
response = client.get_messages(
456
'baz', date_range='2012-01-01..2012-01-31')
457
ids = sorted(m['message_id'] for m in response['messages'])
458
self.assertEqual(['bar', 'naf', 'qux'], ids)
460
def test_date_range_unparsabledaterange(self):
461
client = GrackleClient('localhost', 8449)
462
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
463
with ForkedFakeService.from_client(client, archive):
464
with ExpectedException(UnparsableDateRange, ''):
465
client.get_messages('baz', date_range='2012-01-01')
467
def test_date_range_unparsabledaterange_missing_part(self):
468
client = GrackleClient('localhost', 8450)
469
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
470
with ForkedFakeService.from_client(client, archive):
471
with ExpectedException(UnparsableDateRange, ''):
472
client.get_messages('baz', date_range='2012-01-01..')
474
def test_date_range_unparsabledaterange_extra_part(self):
475
client = GrackleClient('localhost', 8451)
476
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
477
with ForkedFakeService.from_client(client, archive):
478
with ExpectedException(UnparsableDateRange, ''):
479
client.get_messages('baz', date_range='2012-01..12-02..12-03')