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
from grackle.client import (
20
from grackle.client import GrackleClient
21
from grackle.error import (
24
UnsupportedDisplayType,
24
def __init__(self, port, messages=None):
27
from grackle.store import (
33
def make_message(message_id, body='body', headers=None, hidden=False):
37
'Message-Id': message_id,
43
message_headers.update(headers.items())
45
message.set_payload(body)
46
for key, value in message_headers.items():
48
return make_json_message(message_id, message.as_string(), hidden)
51
def make_mime_message(message_id, body='body', headers=None, hidden=False,
52
attachment_type=None):
53
parts = MIMEMultipart()
54
parts.attach(MIMEText(body))
55
if attachment_type is not None:
56
attachment = Message()
57
attachment.set_payload('attactment data.')
58
attachment['Content-Type'] = attachment_type
59
attachment['Content-Disposition'] = 'attachment; filename="file.ext"'
60
parts.attach(attachment)
61
return make_message(message_id, parts.as_string(), headers, hidden)
64
class ForkedFakeService:
65
"""A Grackle service fake, as a ContextManager."""
67
def __init__(self, port, message_archives=None, write_logs=False):
70
:param port: The tcp port to use.
71
:param message_archives: A dict of lists of dicts representing
72
archives of messages. The outer dict represents the archive,
73
the list represents the list of messages for that archive.
74
:param write_logs: If true, log messages will be written to stdout.
27
self.messages = messages
78
if message_archives is None:
79
self.message_archives = {}
81
self.message_archives = message_archives
28
82
self.read_end, self.write_end = os.pipe()
83
self.write_logs = write_logs
86
def from_client(client, message_archives=None):
87
"""Instantiate a ForkedFakeService from the client.
89
:param port: The client to provide service for.
90
:param message_archives: A dict of lists of dicts representing
91
archives of messages. The outer dict represents the archive,
92
the list represents the list of messages for that archive.
94
return ForkedFakeService(client.port, message_archives)
30
96
def is_ready(self):
97
"""Tell the parent process that the server is ready for writes."""
31
98
os.write(self.write_end, 'asdf')
33
100
def __enter__(self):
103
Fork and start a server in the child. Return when the server is ready
36
107
self.start_server()
41
112
def start_server(self):
113
"""Start the HTTP server."""
42
114
service = HTTPServer(('', self.port), FakeGrackleRequestHandler)
43
service.messages = self.messages
115
service.store = MemoryStore(self.message_archives)
119
stream=sys.stderr, level=logging.INFO)
45
120
service.serve_forever()
47
122
def __exit__(self, exc_type, exc_val, traceback):
48
123
os.kill(self.pid, SIGKILL)
51
SUPPORTED_ORDERS = set(['date'])
54
126
class FakeGrackleRequestHandler(BaseHTTPRequestHandler):
127
"""A request handler that forwards to server.store."""
129
def __init__(self, *args, **kwargs):
130
"""Constructor. Sets up logging."""
131
self.logger = logging.getLogger('http')
132
BaseHTTPRequestHandler.__init__(self, *args, **kwargs)
135
"""Create an archive or message on PUT."""
136
scheme, netloc, path, params, query_string, fragments = (
138
parts = path.split('/')
139
if parts[1] != 'archive':
140
# This is an unknonwn operation?
143
# This expected path is /archive/archive_id.
145
self.server.store.put_archive(parts[2])
146
self.send_response(httplib.CREATED)
149
except Exception, error:
151
httplib.BAD_REQUEST, error.__doc__)
153
# This expected path is /archive/archive_id/message_id.
155
message = self.rfile.read(int(self.headers['content-length']))
156
self.server.store.put_message(parts[2], parts[3], message)
157
self.send_response(httplib.CREATED)
161
self.send_error(httplib.BAD_REQUEST)
56
163
def do_POST(self):
57
message = self.rfile.read(int(self.headers['content-length']))
58
if message == 'This is a message':
59
self.send_response(httplib.CREATED)
63
self.send_error(httplib.BAD_REQUEST)
164
"""Change a message on POST."""
165
scheme, netloc, path, params, query_string, fragments = (
167
parts = path.split('/')
168
if parts[1] != 'archive':
169
# This is an unknonwn operation?
172
# This expected path is /archive/archive_id/message_id.
174
# This expected path is /archive/archive_id/message_id.
175
response = self.server.store.hide_message(
176
parts[2], parts[3], query_string)
177
self.send_response(httplib.OK)
179
self.wfile.write(simplejson.dumps(response))
181
self.send_error(httplib.BAD_REQUEST)
184
"""Retrieve a list of messages on GET."""
66
185
scheme, netloc, path, params, query_string, fragments = (
67
186
urlparse(self.path))
68
archive = os.path.split(path)[1]
69
query = parse_qs(query_string)
70
parameters = simplejson.loads(query['parameters'][0])
71
messages = [m for m in self.server.messages[archive] if 'message_ids'
72
not in parameters or m['message_id'] in
73
parameters['message_ids']]
74
if 'order' in parameters:
75
if parameters['order'] not in SUPPORTED_ORDERS:
76
self.send_response(httplib.BAD_REQUEST)
77
self.wfile.write('Unsupported order')
187
parts = path.split('/')
188
if parts[1] == 'archive':
190
response = self.server.store.get_messages(
191
parts[2], query_string)
192
self.send_response(httplib.OK)
194
self.wfile.write(simplejson.dumps(response))
195
except Exception, error:
197
httplib.BAD_REQUEST, error.__doc__)
79
messages.sort(key=lambda m: m[parameters['order']])
80
self.send_response(httplib.OK)
82
limit = parameters.get('limit', 100)
83
memo = parameters.get('memo')
84
message_id_indices = dict(
85
(m['message_id'], idx) for idx, m in enumerate(messages))
89
start = message_id_indices[memo.encode('rot13')]
91
previous_memo = messages[start - 1]['message_id'].encode('rot13')
94
end = min(start + limit, len(messages))
95
if end < len(messages):
96
next_memo = messages[end]['message_id'].encode('rot13')
99
messages = messages[start:end]
101
'messages': messages,
102
'next_memo': next_memo,
103
'previous_memo': previous_memo
105
self.wfile.write(simplejson.dumps(response))
108
def fake_grackle_service(client, messages=None):
111
return ForkedFake(client.port, messages)
200
def log_message(self, format, *args):
201
"""Override log_message to use standard Python logging."""
202
message = "%s - - [%s] %s\n" % (
203
self.address_string(), self.log_date_time_string(), format % args)
204
self.logger.info(message)
207
class TestPutArchive(TestCase):
209
def test_put_archive(self):
210
client = GrackleClient('localhost', 8410)
211
message_archives = {}
212
with ForkedFakeService.from_client(client, message_archives):
213
client.put_archive('arch1')
214
response = client.get_messages('arch1')
215
self.assertEqual(0, len(response['messages']))
217
def test_put_archive_existing_archive(self):
218
client = GrackleClient('localhost', 8411)
219
message_archives = {'arch1': []}
220
with ForkedFakeService.from_client(client, message_archives):
221
with ExpectedException(ArchiveIdExists, ''):
222
client.put_archive('arch1')
114
225
class TestPutMessage(TestCase):
116
227
def test_put_message(self):
117
client = GrackleClient('localhost', 8436)
118
with fake_grackle_service(client):
119
client.put_message('arch1', 'asdf', StringIO('This is a message'))
228
client = GrackleClient('localhost', 8420)
229
message_archives = {'arch1': []}
230
with ForkedFakeService.from_client(client, message_archives):
231
client.put_message('arch1', 'id1', StringIO('This is a message'))
232
response = client.get_messages('arch1')
233
self.assertEqual(1, len(response['messages']))
234
message = response['messages'][0]
235
self.assertEqual('id1', message['message_id'])
237
def test_put_message_without_archive(self):
238
client = GrackleClient('localhost', 8421)
239
message_archives = {'arch1': []}
240
with ForkedFakeService.from_client(client, message_archives):
120
241
with ExpectedException(Exception, 'wtf'):
121
client.put_message('arch1', 'asdf',
122
StringIO('This is not a message'))
242
client.put_message('no-archive', 'id1', StringIO('message'))
125
245
class TestGetMessages(TestCase):
168
284
messages.extend(response['messages'])
169
285
self.assertMessageIDs(['foo', 'bar'], messages)
171
def test_get_messages_date_order(self):
287
def get_messages_member_order_test(self, key):
172
288
client = GrackleClient('localhost', 8439)
173
with fake_grackle_service(client,
174
{'baz': [{'message_id': 'foo', 'date': '2011-03-25'},
175
{'message_id': 'bar', 'date': '2011-03-24'}]}):
295
make_message('foo', headers={header_name: '2011-03-25'}),
296
make_message('bar', headers={header_name: '2011-03-24'}),
298
with ForkedFakeService.from_client(client, archive):
176
299
response = client.get_messages('baz')
177
300
self.assertIDOrder(['foo', 'bar'], response['messages'])
178
response = client.get_messages('baz', order='date')
301
response = client.get_messages('baz', order=key)
179
302
self.assertIDOrder(['bar', 'foo'], response['messages'])
304
def test_get_messages_date_order(self):
305
self.get_messages_member_order_test('date')
307
def test_get_messages_author_order(self):
308
self.get_messages_member_order_test('author')
310
def test_get_messages_subject_order(self):
311
self.get_messages_member_order_test('subject')
313
def test_get_messages_thread_subject_order(self):
316
make_message('bar', headers={'subject': 'y'}),
317
make_message('qux', headers={'subject': 'z'}),
318
make_message('foo', headers={'subject': 'x',
319
'in-reply-to': 'qux'}),
321
client = GrackleClient('localhost', 8439)
322
with ForkedFakeService.from_client(client, archive):
323
response = client.get_messages('baz')
324
self.assertIDOrder(['bar', 'qux', 'foo'], response['messages'])
325
response = client.get_messages('baz', order='subject')
326
self.assertIDOrder(['foo', 'bar', 'qux'], response['messages'])
327
response = client.get_messages('baz', order='thread_subject')
328
self.assertIDOrder(['bar', 'qux', 'foo'], response['messages'])
330
def test_get_messages_thread_oldest_order(self):
331
client = GrackleClient('localhost', 8439)
334
make_message('bar', headers={'date': 'x'}),
335
make_message('qux', headers={'date': 'z'}),
336
make_message('foo', headers={'date': 'y',
337
'in-reply-to': 'qux'}),
339
with ForkedFakeService.from_client(client, archive):
340
response = client.get_messages('baz')
341
self.assertIDOrder(['bar', 'qux', 'foo'], response['messages'])
342
response = client.get_messages('baz', order='date')
343
self.assertIDOrder(['bar', 'foo', 'qux'], response['messages'])
344
response = client.get_messages('baz', order='thread_oldest')
345
self.assertIDOrder(['bar', 'qux', 'foo'], response['messages'])
347
def test_get_messages_thread_newest_order(self):
348
client = GrackleClient('localhost', 8439)
351
make_message('bar', headers={'date': 'x'}),
352
make_message('qux', headers={'date': 'w'}),
353
make_message('foo', headers={'date': 'y',
354
'in-reply-to': 'bar'}),
355
make_message('baz', headers={'date': 'z',
356
'in-reply-to': 'qux'}),
358
with ForkedFakeService.from_client(client, archive):
359
response = client.get_messages('baz', order='date')
361
['qux', 'bar', 'foo', 'baz'], response['messages'])
362
response = client.get_messages('baz', order='thread_newest')
364
['bar', 'foo', 'qux', 'baz'], response['messages'])
181
366
def test_get_messages_unsupported_order(self):
182
367
client = GrackleClient('localhost', 8439)
183
with fake_grackle_service(client,
184
{'baz': [{'message_id': 'foo', 'date': '2011-03-25'},
185
{'message_id': 'bar', 'date': '2011-03-24'}]}):
186
with ExpectedException(UnsupportedOrder):
370
make_message('foo', headers={'date': '2011-03-25'}),
371
make_message('foo', headers={'date': '2011-03-24'}),
373
with ForkedFakeService.from_client(client, archive):
374
with ExpectedException(UnsupportedOrder, ''):
187
375
client.get_messages('baz', order='nonsense')
377
def test_get_messages_headers_no_headers(self):
378
client = GrackleClient('localhost', 8440)
379
archive = {'baz': [make_message('foo')]}
380
with ForkedFakeService.from_client(client, archive):
381
response = client.get_messages('baz', headers=[
382
'Subject', 'Date', 'X-Launchpad-Message-Rationale'])
383
first_message = response['messages'][0]
384
self.assertEqual('foo', first_message['message_id'])
385
self.assertEqual({}, first_message['headers'])
387
def test_get_messages_headers_exclude_headers(self):
388
client = GrackleClient('localhost', 8441)
390
'baz': [make_message('foo', headers={'From': 'me'})]}
391
with ForkedFakeService.from_client(client, archive):
392
response = client.get_messages('baz', headers=[
393
'Subject', 'Date', 'X-Launchpad-Message-Rationale'])
394
first_message = response['messages'][0]
395
self.assertEqual('foo', first_message['message_id'])
396
self.assertEqual({}, first_message['headers'])
398
def test_get_messages_headers_include_headers(self):
399
client = GrackleClient('localhost', 8442)
402
make_message('foo', headers={'From': 'me', 'To': 'you'})]}
403
with ForkedFakeService.from_client(client, archive):
404
response = client.get_messages('baz', headers=[
406
first_message = response['messages'][0]
407
self.assertEqual('foo', first_message['message_id'])
408
self.assertEqual({'From': 'me', 'To': 'you'}, first_message['headers'])
410
def test_get_messages_max_body_length(self):
411
client = GrackleClient('localhost', 8443)
412
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
413
with ForkedFakeService.from_client(client, archive):
414
response = client.get_messages('baz', max_body_length=3)
415
first_message = response['messages'][0]
416
self.assertEqual('abc', first_message['body'])
418
def test_include_hidden(self):
419
client = GrackleClient('localhost', 8444)
422
make_message('foo', hidden=True),
423
make_message('bar', hidden=False),
425
with ForkedFakeService.from_client(client, archive):
426
response = client.get_messages('baz', include_hidden=True)
427
self.assertMessageIDs(['bar', 'foo'], response['messages'])
428
response = client.get_messages('baz', include_hidden=False)
429
self.assertMessageIDs(['bar'], response['messages'])
431
def test_display_type_unknown_value(self):
432
client = GrackleClient('localhost', 8445)
433
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
434
with ForkedFakeService.from_client(client, archive):
435
with ExpectedException(UnsupportedDisplayType, ''):
436
client.get_messages('baz', display_type='unknown')
438
def test_display_type_headers_only(self):
439
client = GrackleClient('localhost', 8446)
442
make_message('foo', body=u'abcdefghi',
443
headers={'From': 'me', 'To': 'you'})]}
444
with ForkedFakeService.from_client(client, archive):
445
response = client.get_messages('baz', display_type='headers-only')
446
first_message = response['messages'][0]
447
self.assertEqual('foo', first_message['message_id'])
449
archive['baz'][0]['headers'], first_message['headers'])
450
self.assertNotIn('body', first_message)
452
def test_display_type_text_only(self):
453
client = GrackleClient('localhost', 8446)
458
headers={'From': 'me', 'To': 'you'},
459
attachment_type='text/x-diff')]}
460
with ForkedFakeService.from_client(client, archive):
461
response = client.get_messages('baz', display_type='text-only')
462
first_message = response['messages'][0]
463
self.assertEqual('foo', first_message['message_id'])
464
self.assertEqual('me', first_message['headers']['From'])
465
self.assertEqual('you', first_message['headers']['To'])
466
self.assertEqual(archive['baz'][0]['body'], first_message['body'])
468
def test_display_type_all(self):
469
client = GrackleClient('localhost', 8447)
474
headers={'From': 'me', 'To': 'you'},
475
attachment_type='text/x-diff')]}
476
with ForkedFakeService.from_client(client, archive):
477
response = client.get_messages('baz', display_type='all')
478
first_message = response['messages'][0]
479
self.assertEqual('foo', first_message['message_id'])
480
self.assertEqual('me', first_message['headers']['From'])
481
self.assertEqual('you', first_message['headers']['To'])
482
self.assertEqual(archive['baz'][0]['body'], first_message['body'])
484
def test_date_range(self):
485
client = GrackleClient('localhost', 8448)
489
'foo', 'abcdefghi', headers={'date': '2011-12-31'}),
491
'bar', 'abcdefghi', headers={'date': '2012-01-01'}),
493
'qux', 'abcdefghi', headers={'date': '2012-01-15'}),
495
'naf', 'abcdefghi', headers={'date': '2012-01-31'}),
497
'doh', 'abcdefghi', headers={'date': '2012-02-01'}),
499
with ForkedFakeService.from_client(client, archive):
500
response = client.get_messages(
501
'baz', date_range='2012-01-01..2012-01-31')
502
ids = sorted(m['message_id'] for m in response['messages'])
503
self.assertEqual(['bar', 'naf', 'qux'], ids)
505
def test_date_range_unparsabledaterange(self):
506
client = GrackleClient('localhost', 8449)
507
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
508
with ForkedFakeService.from_client(client, archive):
509
with ExpectedException(UnparsableDateRange, ''):
510
client.get_messages('baz', date_range='2012-01-01')
512
def test_date_range_unparsabledaterange_missing_part(self):
513
client = GrackleClient('localhost', 8450)
514
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
515
with ForkedFakeService.from_client(client, archive):
516
with ExpectedException(UnparsableDateRange, ''):
517
client.get_messages('baz', date_range='2012-01-01..')
519
def test_date_range_unparsabledaterange_extra_part(self):
520
client = GrackleClient('localhost', 8451)
521
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
522
with ForkedFakeService.from_client(client, archive):
523
with ExpectedException(UnparsableDateRange, ''):
524
client.get_messages('baz', date_range='2012-01..12-02..12-03')
527
class TestHideMessages(TestCase):
529
def test_hide_message_true(self):
530
client = GrackleClient('localhost', 8470)
533
make_message('foo', hidden=False),
535
with ForkedFakeService.from_client(client, archive):
536
response = client.hide_message('baz', 'foo', hidden=True)
537
self.assertEqual('foo', response['message_id'])
538
self.assertIs(True, response['hidden'])
540
def test_hide_message_false(self):
541
client = GrackleClient('localhost', 8470)
544
make_message('foo', hidden=True),
546
with ForkedFakeService.from_client(client, archive):
547
response = client.hide_message('baz', 'foo', hidden=False)
548
self.assertEqual('foo', response['message_id'])
549
self.assertIs(False, response['hidden'])