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 (
23
def __init__(self, port, messages=None):
20
from grackle.client import GrackleClient
21
from grackle.error import (
24
UnsupportedDisplayType,
27
from grackle.service import ForkedFakeService
28
from grackle.store import (
34
def make_message(message_id, body='body', headers=None, hidden=False):
38
'Message-Id': message_id,
44
message_headers.update(headers.items())
46
message.set_payload(body)
47
for key, value in message_headers.items():
49
return make_json_message(message_id, message.as_string(), hidden)
52
def make_mime_message(message_id, body='body', headers=None, hidden=False,
53
attachment_type=None):
54
parts = MIMEMultipart()
55
parts.attach(MIMEText(body))
56
if attachment_type is not None:
57
attachment = Message()
58
attachment.set_payload('attactment data.')
59
attachment['Content-Type'] = attachment_type
60
attachment['Content-Disposition'] = 'attachment; filename="file.ext"'
61
parts.attach(attachment)
62
return make_message(message_id, parts.as_string(), headers, hidden)
65
class XXXForkedFakeService:
66
"""A Grackle service fake, as a ContextManager."""
68
def __init__(self, port, message_archives=None, write_logs=False):
71
:param port: The tcp port to use.
72
:param message_archives: A dict of lists of dicts representing
73
archives of messages. The outer dict represents the archive,
74
the list represents the list of messages for that archive.
75
:param write_logs: If true, log messages will be written to stdout.
26
self.messages = messages
79
if message_archives is None:
80
self.message_archives = {}
82
self.message_archives = message_archives
27
83
self.read_end, self.write_end = os.pipe()
84
self.write_logs = write_logs
87
def from_client(client, message_archives=None):
88
"""Instantiate a ForkedFakeService from the client.
90
:param port: The client to provide service for.
91
:param message_archives: A dict of lists of dicts representing
92
archives of messages. The outer dict represents the archive,
93
the list represents the list of messages for that archive.
95
return ForkedFakeService(client.port, message_archives)
29
97
def is_ready(self):
98
"""Tell the parent process that the server is ready for writes."""
30
99
os.write(self.write_end, 'asdf')
32
101
def __enter__(self):
104
Fork and start a server in the child. Return when the server is ready
35
108
self.start_server()
50
127
class FakeGrackleRequestHandler(BaseHTTPRequestHandler):
128
"""A request handler that forwards to server.store."""
130
def __init__(self, *args, **kwargs):
131
"""Constructor. Sets up logging."""
132
self.logger = logging.getLogger('http')
133
BaseHTTPRequestHandler.__init__(self, *args, **kwargs)
136
"""Create an archive or message on PUT."""
137
scheme, netloc, path, params, query_string, fragments = (
139
parts = path.split('/')
140
if parts[1] != 'archive':
141
# This is an unknonwn operation?
144
# This expected path is /archive/archive_id.
146
self.server.store.put_archive(parts[2])
147
self.send_response(httplib.CREATED)
150
except Exception, error:
152
httplib.BAD_REQUEST, error.__doc__)
154
# This expected path is /archive/archive_id/message_id.
156
message = self.rfile.read(int(self.headers['content-length']))
157
self.server.store.put_message(parts[2], parts[3], message)
158
self.send_response(httplib.CREATED)
162
self.send_error(httplib.BAD_REQUEST)
52
164
def do_POST(self):
53
message = self.rfile.read(int(self.headers['content-length']))
54
if message == 'This is a message':
55
self.send_response(httplib.CREATED)
59
self.send_error(httplib.BAD_REQUEST)
165
"""Change a message on POST."""
166
scheme, netloc, path, params, query_string, fragments = (
168
parts = path.split('/')
169
if parts[1] != 'archive':
170
# This is an unknonwn operation?
173
# This expected path is /archive/archive_id/message_id.
175
# This expected path is /archive/archive_id/message_id.
176
response = self.server.store.hide_message(
177
parts[2], parts[3], query_string)
178
self.send_response(httplib.OK)
180
self.wfile.write(simplejson.dumps(response))
182
self.send_error(httplib.BAD_REQUEST)
185
"""Retrieve a list of messages on GET."""
62
186
scheme, netloc, path, params, query_string, fragments = (
63
187
urlparse(self.path))
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)
188
parts = path.split('/')
189
if parts[1] == 'archive':
191
response = self.server.store.get_messages(
192
parts[2], query_string)
193
self.send_response(httplib.OK)
195
self.wfile.write(simplejson.dumps(response))
196
except Exception, error:
198
httplib.BAD_REQUEST, error.__doc__)
201
def log_message(self, format, *args):
202
"""Override log_message to use standard Python logging."""
203
message = "%s - - [%s] %s\n" % (
204
self.address_string(), self.log_date_time_string(), format % args)
205
self.logger.info(message)
208
class TestPutArchive(TestCase):
210
def test_put_archive(self):
211
client = GrackleClient('localhost', 8410)
212
message_archives = {}
213
with ForkedFakeService.from_client(client, message_archives):
214
client.put_archive('arch1')
215
response = client.get_messages('arch1')
216
self.assertEqual(0, len(response['messages']))
218
def test_put_archive_existing_archive(self):
219
client = GrackleClient('localhost', 8411)
220
message_archives = {'arch1': []}
221
with ForkedFakeService.from_client(client, message_archives):
222
with ExpectedException(ArchiveIdExists, ''):
223
client.put_archive('arch1')
104
226
class TestPutMessage(TestCase):
106
228
def test_put_message(self):
107
client = GrackleClient('localhost', 8436)
108
with fake_grackle_service(client):
109
client.put_message('arch1', 'asdf', StringIO('This is a message'))
229
client = GrackleClient('localhost', 8420)
230
message_archives = {'arch1': []}
231
with ForkedFakeService.from_client(client, message_archives):
232
client.put_message('arch1', 'id1', StringIO('This is a message'))
233
response = client.get_messages('arch1')
234
self.assertEqual(1, len(response['messages']))
235
message = response['messages'][0]
236
self.assertEqual('id1', message['message_id'])
238
def test_put_message_without_archive(self):
239
client = GrackleClient('localhost', 8421)
240
message_archives = {'arch1': []}
241
with ForkedFakeService.from_client(client, message_archives):
110
242
with ExpectedException(Exception, 'wtf'):
111
client.put_message('arch1', 'asdf',
112
StringIO('This is not a message'))
243
client.put_message('no-archive', 'id1', StringIO('message'))
115
246
class TestGetMessages(TestCase):
248
def assertIDOrder(self, ids, messages):
249
self.assertEqual(ids, [m['message_id'] for m in messages])
117
251
def assertMessageIDs(self, ids, messages):
119
sorted(ids), sorted(m['message_id'] for m in messages))
253
sorted(ids), sorted(messages, key=lambda m: m['message_id']))
121
255
def test_get_messages(self):
122
client = GrackleClient('localhost', 8435)
123
with fake_grackle_service(client,
125
[{'message_id': 'foo'},
126
{'message_id': 'bar'}]}):
256
client = GrackleClient('localhost', 8430)
258
'baz': [make_message('foo'), make_message('bar')]}
259
with ForkedFakeService.from_client(client, archive):
127
260
response = client.get_messages('baz')
128
261
self.assertEqual(['bar', 'foo'], sorted(m['message_id'] for m in
129
262
response['messages']))
154
284
self.assertEqual(1, len(response['messages']))
155
285
messages.extend(response['messages'])
156
286
self.assertMessageIDs(['foo', 'bar'], messages)
288
def get_messages_member_order_test(self, key):
289
client = GrackleClient('localhost', 8439)
296
make_message('foo', headers={header_name: '2011-03-25'}),
297
make_message('bar', headers={header_name: '2011-03-24'}),
299
with ForkedFakeService.from_client(client, archive):
300
response = client.get_messages('baz')
301
self.assertIDOrder(['foo', 'bar'], response['messages'])
302
response = client.get_messages('baz', order=key)
303
self.assertIDOrder(['bar', 'foo'], response['messages'])
305
def test_get_messages_date_order(self):
306
self.get_messages_member_order_test('date')
308
def test_get_messages_author_order(self):
309
self.get_messages_member_order_test('author')
311
def test_get_messages_subject_order(self):
312
self.get_messages_member_order_test('subject')
314
def test_get_messages_thread_subject_order(self):
317
make_message('bar', headers={'subject': 'y'}),
318
make_message('qux', headers={'subject': 'z'}),
319
make_message('foo', headers={'subject': 'x',
320
'in-reply-to': 'qux'}),
322
client = GrackleClient('localhost', 8439)
323
with ForkedFakeService.from_client(client, archive):
324
response = client.get_messages('baz')
325
self.assertIDOrder(['bar', 'qux', 'foo'], response['messages'])
326
response = client.get_messages('baz', order='subject')
327
self.assertIDOrder(['foo', 'bar', 'qux'], response['messages'])
328
response = client.get_messages('baz', order='thread_subject')
329
self.assertIDOrder(['bar', 'qux', 'foo'], response['messages'])
331
def test_get_messages_thread_oldest_order(self):
332
client = GrackleClient('localhost', 8439)
335
make_message('bar', headers={'date': 'x'}),
336
make_message('qux', headers={'date': 'z'}),
337
make_message('foo', headers={'date': 'y',
338
'in-reply-to': 'qux'}),
340
with ForkedFakeService.from_client(client, archive):
341
response = client.get_messages('baz')
342
self.assertIDOrder(['bar', 'qux', 'foo'], response['messages'])
343
response = client.get_messages('baz', order='date')
344
self.assertIDOrder(['bar', 'foo', 'qux'], response['messages'])
345
response = client.get_messages('baz', order='thread_oldest')
346
self.assertIDOrder(['bar', 'qux', 'foo'], response['messages'])
348
def test_get_messages_thread_newest_order(self):
349
client = GrackleClient('localhost', 8439)
352
make_message('bar', headers={'date': 'x'}),
353
make_message('qux', headers={'date': 'w'}),
354
make_message('foo', headers={'date': 'y',
355
'in-reply-to': 'bar'}),
356
make_message('baz', headers={'date': 'z',
357
'in-reply-to': 'qux'}),
359
with ForkedFakeService.from_client(client, archive):
360
response = client.get_messages('baz', order='date')
362
['qux', 'bar', 'foo', 'baz'], response['messages'])
363
response = client.get_messages('baz', order='thread_newest')
365
['bar', 'foo', 'qux', 'baz'], response['messages'])
367
def test_get_messages_unsupported_order(self):
368
client = GrackleClient('localhost', 8439)
371
make_message('foo', headers={'date': '2011-03-25'}),
372
make_message('foo', headers={'date': '2011-03-24'}),
374
with ForkedFakeService.from_client(client, archive):
375
with ExpectedException(UnsupportedOrder, ''):
376
client.get_messages('baz', order='nonsense')
378
def test_get_messages_headers_no_headers(self):
379
client = GrackleClient('localhost', 8440)
380
archive = {'baz': [make_message('foo')]}
381
with ForkedFakeService.from_client(client, archive):
382
response = client.get_messages('baz', headers=[
383
'Subject', 'Date', 'X-Launchpad-Message-Rationale'])
384
first_message = response['messages'][0]
385
self.assertEqual('foo', first_message['message_id'])
386
self.assertEqual({}, first_message['headers'])
388
def test_get_messages_headers_exclude_headers(self):
389
client = GrackleClient('localhost', 8441)
391
'baz': [make_message('foo', headers={'From': 'me'})]}
392
with ForkedFakeService.from_client(client, archive):
393
response = client.get_messages('baz', headers=[
394
'Subject', 'Date', 'X-Launchpad-Message-Rationale'])
395
first_message = response['messages'][0]
396
self.assertEqual('foo', first_message['message_id'])
397
self.assertEqual({}, first_message['headers'])
399
def test_get_messages_headers_include_headers(self):
400
client = GrackleClient('localhost', 8442)
403
make_message('foo', headers={'From': 'me', 'To': 'you'})]}
404
with ForkedFakeService.from_client(client, archive):
405
response = client.get_messages('baz', headers=[
407
first_message = response['messages'][0]
408
self.assertEqual('foo', first_message['message_id'])
409
self.assertEqual({'From': 'me', 'To': 'you'}, first_message['headers'])
411
def test_get_messages_max_body_length(self):
412
client = GrackleClient('localhost', 8443)
413
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
414
with ForkedFakeService.from_client(client, archive):
415
response = client.get_messages('baz', max_body_length=3)
416
first_message = response['messages'][0]
417
self.assertEqual('abc', first_message['body'])
419
def test_include_hidden(self):
420
client = GrackleClient('localhost', 8444)
423
make_message('foo', hidden=True),
424
make_message('bar', hidden=False),
426
with ForkedFakeService.from_client(client, archive):
427
response = client.get_messages('baz', include_hidden=True)
428
self.assertMessageIDs(['bar', 'foo'], response['messages'])
429
response = client.get_messages('baz', include_hidden=False)
430
self.assertMessageIDs(['bar'], response['messages'])
432
def test_display_type_unknown_value(self):
433
client = GrackleClient('localhost', 8445)
434
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
435
with ForkedFakeService.from_client(client, archive):
436
with ExpectedException(UnsupportedDisplayType, ''):
437
client.get_messages('baz', display_type='unknown')
439
def test_display_type_headers_only(self):
440
client = GrackleClient('localhost', 8446)
443
make_message('foo', body=u'abcdefghi',
444
headers={'From': 'me', 'To': 'you'})]}
445
with ForkedFakeService.from_client(client, archive):
446
response = client.get_messages('baz', display_type='headers-only')
447
first_message = response['messages'][0]
448
self.assertEqual('foo', first_message['message_id'])
450
archive['baz'][0]['headers'], first_message['headers'])
451
self.assertNotIn('body', first_message)
453
def test_display_type_text_only(self):
454
client = GrackleClient('localhost', 8446)
459
headers={'From': 'me', 'To': 'you'},
460
attachment_type='text/x-diff')]}
461
with ForkedFakeService.from_client(client, archive):
462
response = client.get_messages('baz', display_type='text-only')
463
first_message = response['messages'][0]
464
self.assertEqual('foo', first_message['message_id'])
465
self.assertEqual('me', first_message['headers']['From'])
466
self.assertEqual('you', first_message['headers']['To'])
467
self.assertEqual(archive['baz'][0]['body'], first_message['body'])
469
def test_display_type_all(self):
470
client = GrackleClient('localhost', 8447)
475
headers={'From': 'me', 'To': 'you'},
476
attachment_type='text/x-diff')]}
477
with ForkedFakeService.from_client(client, archive):
478
response = client.get_messages('baz', display_type='all')
479
first_message = response['messages'][0]
480
self.assertEqual('foo', first_message['message_id'])
481
self.assertEqual('me', first_message['headers']['From'])
482
self.assertEqual('you', first_message['headers']['To'])
483
self.assertEqual(archive['baz'][0]['body'], first_message['body'])
485
def test_date_range(self):
486
client = GrackleClient('localhost', 8448)
490
'foo', 'abcdefghi', headers={'date': '2011-12-31'}),
492
'bar', 'abcdefghi', headers={'date': '2012-01-01'}),
494
'qux', 'abcdefghi', headers={'date': '2012-01-15'}),
496
'naf', 'abcdefghi', headers={'date': '2012-01-31'}),
498
'doh', 'abcdefghi', headers={'date': '2012-02-01'}),
500
with ForkedFakeService.from_client(client, archive):
501
response = client.get_messages(
502
'baz', date_range='2012-01-01..2012-01-31')
503
ids = sorted(m['message_id'] for m in response['messages'])
504
self.assertEqual(['bar', 'naf', 'qux'], ids)
506
def test_date_range_unparsabledaterange(self):
507
client = GrackleClient('localhost', 8449)
508
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
509
with ForkedFakeService.from_client(client, archive):
510
with ExpectedException(UnparsableDateRange, ''):
511
client.get_messages('baz', date_range='2012-01-01')
513
def test_date_range_unparsabledaterange_missing_part(self):
514
client = GrackleClient('localhost', 8450)
515
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
516
with ForkedFakeService.from_client(client, archive):
517
with ExpectedException(UnparsableDateRange, ''):
518
client.get_messages('baz', date_range='2012-01-01..')
520
def test_date_range_unparsabledaterange_extra_part(self):
521
client = GrackleClient('localhost', 8451)
522
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
523
with ForkedFakeService.from_client(client, archive):
524
with ExpectedException(UnparsableDateRange, ''):
525
client.get_messages('baz', date_range='2012-01..12-02..12-03')
528
class TestHideMessages(TestCase):
530
def test_hide_message_true(self):
531
client = GrackleClient('localhost', 8470)
534
make_message('foo', hidden=False),
536
with ForkedFakeService.from_client(client, archive):
537
response = client.hide_message('baz', 'foo', hidden=True)
538
self.assertEqual('foo', response['message_id'])
539
self.assertIs(True, response['hidden'])
541
def test_hide_message_false(self):
542
client = GrackleClient('localhost', 8470)
545
make_message('foo', hidden=True),
547
with ForkedFakeService.from_client(client, archive):
548
response = client.hide_message('baz', 'foo', hidden=False)
549
self.assertEqual('foo', response['message_id'])
550
self.assertIs(False, response['hidden'])