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
8
13
from StringIO import StringIO
9
15
from unittest import TestCase
16
from urlparse import urlparse
11
18
from testtools import ExpectedException
13
from grackle import client
18
def __init__(self, func_or_method):
19
self.func_or_method = func_or_method
20
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.
77
if message_archives is None:
78
self.message_archives = {}
80
self.message_archives = message_archives
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)
96
"""Tell the parent process that the server is ready for writes."""
97
os.write(self.write_end, 'asdf')
22
99
def __enter__(self):
102
Fork and start a server in the child. Return when the server is ready
108
os.read(self.read_end, 1)
111
def start_server(self):
112
"""Start the HTTP server."""
113
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', {})
121
stream=sys.stderr, level=logging.INFO)
122
service.serve_forever()
30
124
def __exit__(self, exc_type, exc_val, traceback):
31
125
os.kill(self.pid, SIGKILL)
34
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)
36
136
def do_POST(self):
37
message = self.rfile.read(int(self.headers['content-length']))
38
if message == 'This is a message':
39
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'
43
self.send_error(httplib.BAD_REQUEST)
47
service = HTTPServer(('', 8435), FakeGrackleRequestHandler)
48
service.serve_forever()
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)
170
"""Retrieve a list of messages on GET."""
171
scheme, netloc, path, params, query_string, fragments = (
173
parts = path.split('/')
174
if parts[1] == 'archive':
176
response = self.server.store.get_messages(
177
parts[2], query_string)
178
self.send_response(httplib.OK)
180
self.wfile.write(simplejson.dumps(response))
181
except Exception, error:
183
httplib.BAD_REQUEST, error.__doc__)
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)
52
193
class TestPutMessage(TestCase):
54
195
def test_put_message(self):
55
with Forked(run_service):
56
client.put_message('arch1', 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):
57
209
with ExpectedException(Exception, 'wtf'):
58
client.put_message('arch1', StringIO('This is not a message'))
210
client.put_message('no-archive', 'id1', StringIO('message'))
213
class TestGetMessages(TestCase):
215
def assertIDOrder(self, ids, messages):
216
self.assertEqual(ids, [m['message_id'] for m in messages])
218
def assertMessageIDs(self, ids, messages):
220
sorted(ids), sorted(messages, key=lambda m: m['message_id']))
222
def test_get_messages(self):
223
client = GrackleClient('localhost', 8430)
225
'baz': [make_message('foo'), make_message('bar')]}
226
with ForkedFakeService.from_client(client, archive):
227
response = client.get_messages('baz')
228
self.assertEqual(['bar', 'foo'], sorted(m['message_id'] for m in
229
response['messages']))
230
self.assertIs(None, response['next_memo'])
231
self.assertIs(None, response['previous_memo'])
233
def test_get_messages_by_id(self):
234
client = GrackleClient('localhost', 8437)
236
'baz': [make_message('foo'), make_message('bar')]}
237
with ForkedFakeService.from_client(client, archive):
238
response = client.get_messages('baz', message_ids=['foo'])
239
message, = response['messages']
240
self.assertEqual('foo', message['message_id'])
242
def test_get_messages_batching(self):
243
client = GrackleClient('localhost', 8438)
244
archive = {'baz': [make_message('foo'), make_message('bar')]}
245
with ForkedFakeService.from_client(client, archive):
246
response = client.get_messages('baz', limit=1)
247
self.assertEqual(1, len(response['messages']))
248
messages = response['messages']
249
response = client.get_messages(
250
'baz', limit=1, memo=response['next_memo'])
251
self.assertEqual(1, len(response['messages']))
252
messages.extend(response['messages'])
253
self.assertMessageIDs(['foo', 'bar'], messages)
255
def get_messages_member_order_test(self, key):
256
client = GrackleClient('localhost', 8439)
263
make_message('foo', headers={header_name: '2011-03-25'}),
264
make_message('bar', headers={header_name: '2011-03-24'}),
266
with ForkedFakeService.from_client(client, archive):
267
response = client.get_messages('baz')
268
self.assertIDOrder(['foo', 'bar'], response['messages'])
269
response = client.get_messages('baz', order=key)
270
self.assertIDOrder(['bar', 'foo'], response['messages'])
272
def test_get_messages_date_order(self):
273
self.get_messages_member_order_test('date')
275
def test_get_messages_author_order(self):
276
self.get_messages_member_order_test('author')
278
def test_get_messages_subject_order(self):
279
self.get_messages_member_order_test('subject')
281
def test_get_messages_thread_subject_order(self):
284
make_message('bar', headers={'subject': 'y'}),
285
make_message('qux', headers={'subject': 'z'}),
286
make_message('foo', headers={'subject': 'x',
287
'in-reply-to': 'qux'}),
289
client = GrackleClient('localhost', 8439)
290
with ForkedFakeService.from_client(client, archive):
291
response = client.get_messages('baz')
292
self.assertIDOrder(['bar', 'qux', 'foo'], response['messages'])
293
response = client.get_messages('baz', order='subject')
294
self.assertIDOrder(['foo', 'bar', 'qux'], response['messages'])
295
response = client.get_messages('baz', order='thread_subject')
296
self.assertIDOrder(['bar', 'qux', 'foo'], response['messages'])
298
def test_get_messages_thread_oldest_order(self):
299
client = GrackleClient('localhost', 8439)
302
make_message('bar', headers={'date': 'x'}),
303
make_message('qux', headers={'date': 'z'}),
304
make_message('foo', headers={'date': 'y',
305
'in-reply-to': 'qux'}),
307
with ForkedFakeService.from_client(client, archive):
308
response = client.get_messages('baz')
309
self.assertIDOrder(['bar', 'qux', 'foo'], response['messages'])
310
response = client.get_messages('baz', order='date')
311
self.assertIDOrder(['bar', 'foo', 'qux'], response['messages'])
312
response = client.get_messages('baz', order='thread_oldest')
313
self.assertIDOrder(['bar', 'qux', 'foo'], response['messages'])
315
def test_get_messages_thread_newest_order(self):
316
client = GrackleClient('localhost', 8439)
319
make_message('bar', headers={'date': 'x'}),
320
make_message('qux', headers={'date': 'w'}),
321
make_message('foo', headers={'date': 'y',
322
'in-reply-to': 'bar'}),
323
make_message('baz', headers={'date': 'z',
324
'in-reply-to': 'qux'}),
326
with ForkedFakeService.from_client(client, archive):
327
response = client.get_messages('baz', order='date')
329
['qux', 'bar', 'foo', 'baz'], response['messages'])
330
response = client.get_messages('baz', order='thread_newest')
332
['bar', 'foo', 'qux', 'baz'], response['messages'])
334
def test_get_messages_unsupported_order(self):
335
client = GrackleClient('localhost', 8439)
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, ''):
343
client.get_messages('baz', order='nonsense')
345
def test_get_messages_headers_no_headers(self):
346
client = GrackleClient('localhost', 8440)
347
archive = {'baz': [make_message('foo')]}
348
with ForkedFakeService.from_client(client, archive):
349
response = client.get_messages('baz', headers=[
350
'Subject', 'Date', 'X-Launchpad-Message-Rationale'])
351
first_message = response['messages'][0]
352
self.assertEqual('foo', first_message['message_id'])
353
self.assertEqual({}, first_message['headers'])
355
def test_get_messages_headers_exclude_headers(self):
356
client = GrackleClient('localhost', 8441)
358
'baz': [make_message('foo', headers={'From': 'me'})]}
359
with ForkedFakeService.from_client(client, archive):
360
response = client.get_messages('baz', headers=[
361
'Subject', 'Date', 'X-Launchpad-Message-Rationale'])
362
first_message = response['messages'][0]
363
self.assertEqual('foo', first_message['message_id'])
364
self.assertEqual({}, first_message['headers'])
366
def test_get_messages_headers_include_headers(self):
367
client = GrackleClient('localhost', 8442)
370
make_message('foo', headers={'From': 'me', 'To': 'you'})]}
371
with ForkedFakeService.from_client(client, archive):
372
response = client.get_messages('baz', headers=[
374
first_message = response['messages'][0]
375
self.assertEqual('foo', first_message['message_id'])
376
self.assertEqual({'From': 'me', 'To': 'you'}, first_message['headers'])
378
def test_get_messages_max_body_length(self):
379
client = GrackleClient('localhost', 8443)
380
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
381
with ForkedFakeService.from_client(client, archive):
382
response = client.get_messages('baz', max_body_length=3)
383
first_message = response['messages'][0]
384
self.assertEqual('abc', first_message['body'])
386
def test_include_hidden(self):
387
client = GrackleClient('localhost', 8444)
390
make_message('foo', hidden=True),
391
make_message('bar', hidden=False),
393
with ForkedFakeService.from_client(client, archive):
394
response = client.get_messages('baz', include_hidden=True)
395
self.assertMessageIDs(['bar', 'foo'], response['messages'])
396
response = client.get_messages('baz', include_hidden=False)
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'])