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(['date', 'author', 'subject', 'thread_subject'])
128
69
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)
137
"""Create an archive or message on PUT."""
138
scheme, netloc, path, params, query_string, fragments = (
140
parts = path.split('/')
141
if parts[1] != 'archive':
142
# This is an unknonwn operation?
145
# This expected path is /archive/archive_id/message_id.
147
message = self.rfile.read(int(self.headers['content-length']))
148
self.server.store.put_message(parts[2], parts[3], message)
149
self.send_response(httplib.CREATED)
153
self.send_error(httplib.BAD_REQUEST)
155
71
def do_POST(self):
156
"""Change a message on POST."""
157
scheme, netloc, path, params, query_string, fragments = (
159
parts = path.split('/')
160
if parts[1] != 'archive':
161
# This is an unknonwn operation?
164
# This expected path is /archive/archive_id/message_id.
166
# This expected path is /archive/archive_id/message_id.
167
response = self.server.store.hide_message(
168
parts[2], parts[3], query_string)
169
self.send_response(httplib.OK)
171
self.wfile.write(simplejson.dumps(response))
173
self.send_error(httplib.BAD_REQUEST)
72
message = self.rfile.read(int(self.headers['content-length']))
73
if message == 'This is a message':
74
self.send_response(httplib.CREATED)
78
self.send_error(httplib.BAD_REQUEST)
176
"""Retrieve a list of messages on GET."""
177
81
scheme, netloc, path, params, query_string, fragments = (
178
82
urlparse(self.path))
179
parts = path.split('/')
180
if parts[1] == 'archive':
182
response = self.server.store.get_messages(
183
parts[2], query_string)
184
self.send_response(httplib.OK)
186
self.wfile.write(simplejson.dumps(response))
187
except Exception, error:
189
httplib.BAD_REQUEST, error.__doc__)
83
archive = os.path.split(path)[1]
84
query = parse_qs(query_string)
85
parameters = simplejson.loads(query['parameters'][0])
86
order = parameters.get('order')
87
messages = self.server.messages[archive]
88
if order is not None :
89
if order not in SUPPORTED_ORDERS:
90
self.send_response(httplib.BAD_REQUEST)
91
self.wfile.write('Unsupported order')
192
def log_message(self, format, *args):
193
"""Override log_message to use standard Python logging."""
194
message = "%s - - [%s] %s\n" % (
195
self.address_string(), self.log_date_time_string(), format % args)
196
self.logger.info(message)
93
elif order == 'thread_subject':
94
threaded = threaded_messages(messages)
96
threaded.sort(key=lambda t: t[0]['subject'])
97
for thread in threaded:
98
messages.extend(thread)
101
self.server.messages[archive], key=lambda m: m[order])
102
messages = [m for m in messages
103
if 'message_ids' not in parameters or
104
m['message_id'] in parameters['message_ids']]
105
self.send_response(httplib.OK)
107
limit = parameters.get('limit', 100)
108
memo = parameters.get('memo')
109
message_id_indices = dict(
110
(m['message_id'], idx) for idx, m in enumerate(messages))
114
start = message_id_indices[memo.encode('rot13')]
116
previous_memo = messages[start - 1]['message_id'].encode('rot13')
119
end = min(start + limit, len(messages))
120
if end < len(messages):
121
next_memo = messages[end]['message_id'].encode('rot13')
124
messages = messages[start:end]
126
'messages': messages,
127
'next_memo': next_memo,
128
'previous_memo': previous_memo
130
self.wfile.write(simplejson.dumps(response))
133
def fake_grackle_service(client, messages=None):
136
return ForkedFake(client.port, messages)
199
139
class TestPutMessage(TestCase):
201
141
def test_put_message(self):
202
client = GrackleClient('localhost', 8420)
203
message_archives = {'arch1': []}
204
with ForkedFakeService.from_client(client, message_archives):
205
client.put_message('arch1', 'id1', StringIO('This is a message'))
206
response = client.get_messages('arch1')
207
self.assertEqual(1, len(response['messages']))
208
message = response['messages'][0]
209
self.assertEqual('id1', message['message_id'])
211
def test_put_message_without_archive(self):
212
client = GrackleClient('localhost', 8421)
213
message_archives = {'arch1': []}
214
with ForkedFakeService.from_client(client, message_archives):
142
client = GrackleClient('localhost', 8436)
143
with fake_grackle_service(client):
144
client.put_message('arch1', 'asdf', StringIO('This is a message'))
215
145
with ExpectedException(Exception, 'wtf'):
216
client.put_message('no-archive', 'id1', StringIO('message'))
146
client.put_message('arch1', 'asdf',
147
StringIO('This is not a message'))
219
150
class TestGetMessages(TestCase):
301
226
response = client.get_messages('baz', order='thread_subject')
302
227
self.assertIDOrder(['bar', 'qux', 'foo'], response['messages'])
304
def test_get_messages_thread_oldest_order(self):
305
client = GrackleClient('localhost', 8439)
308
make_message('bar', headers={'date': 'x'}),
309
make_message('qux', headers={'date': 'z'}),
310
make_message('foo', headers={'date': 'y',
311
'in-reply-to': 'qux'}),
313
with ForkedFakeService.from_client(client, archive):
314
response = client.get_messages('baz')
315
self.assertIDOrder(['bar', 'qux', 'foo'], response['messages'])
316
response = client.get_messages('baz', order='date')
317
self.assertIDOrder(['bar', 'foo', 'qux'], response['messages'])
318
response = client.get_messages('baz', order='thread_oldest')
319
self.assertIDOrder(['bar', 'qux', 'foo'], response['messages'])
321
def test_get_messages_thread_newest_order(self):
322
client = GrackleClient('localhost', 8439)
325
make_message('bar', headers={'date': 'x'}),
326
make_message('qux', headers={'date': 'w'}),
327
make_message('foo', headers={'date': 'y',
328
'in-reply-to': 'bar'}),
329
make_message('baz', headers={'date': 'z',
330
'in-reply-to': 'qux'}),
332
with ForkedFakeService.from_client(client, archive):
333
response = client.get_messages('baz', order='date')
335
['qux', 'bar', 'foo', 'baz'], response['messages'])
336
response = client.get_messages('baz', order='thread_newest')
338
['bar', 'foo', 'qux', 'baz'], response['messages'])
340
229
def test_get_messages_unsupported_order(self):
341
230
client = GrackleClient('localhost', 8439)
344
make_message('foo', headers={'date': '2011-03-25'}),
345
make_message('foo', headers={'date': '2011-03-24'}),
347
with ForkedFakeService.from_client(client, archive):
348
with ExpectedException(UnsupportedOrder, ''):
231
with fake_grackle_service(client,
232
{'baz': [{'message_id': 'foo', 'date': '2011-03-25'},
233
{'message_id': 'bar', 'date': '2011-03-24'}]}):
234
with ExpectedException(UnsupportedOrder):
349
235
client.get_messages('baz', order='nonsense')
351
def test_get_messages_headers_no_headers(self):
352
client = GrackleClient('localhost', 8440)
353
archive = {'baz': [make_message('foo')]}
354
with ForkedFakeService.from_client(client, archive):
355
response = client.get_messages('baz', headers=[
356
'Subject', 'Date', 'X-Launchpad-Message-Rationale'])
357
first_message = response['messages'][0]
358
self.assertEqual('foo', first_message['message_id'])
359
self.assertEqual({}, first_message['headers'])
361
def test_get_messages_headers_exclude_headers(self):
362
client = GrackleClient('localhost', 8441)
364
'baz': [make_message('foo', headers={'From': 'me'})]}
365
with ForkedFakeService.from_client(client, archive):
366
response = client.get_messages('baz', headers=[
367
'Subject', 'Date', 'X-Launchpad-Message-Rationale'])
368
first_message = response['messages'][0]
369
self.assertEqual('foo', first_message['message_id'])
370
self.assertEqual({}, first_message['headers'])
372
def test_get_messages_headers_include_headers(self):
373
client = GrackleClient('localhost', 8442)
376
make_message('foo', headers={'From': 'me', 'To': 'you'})]}
377
with ForkedFakeService.from_client(client, archive):
378
response = client.get_messages('baz', headers=[
380
first_message = response['messages'][0]
381
self.assertEqual('foo', first_message['message_id'])
382
self.assertEqual({'From': 'me', 'To': 'you'}, first_message['headers'])
384
def test_get_messages_max_body_length(self):
385
client = GrackleClient('localhost', 8443)
386
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
387
with ForkedFakeService.from_client(client, archive):
388
response = client.get_messages('baz', max_body_length=3)
389
first_message = response['messages'][0]
390
self.assertEqual('abc', first_message['body'])
392
def test_include_hidden(self):
393
client = GrackleClient('localhost', 8444)
396
make_message('foo', hidden=True),
397
make_message('bar', hidden=False),
399
with ForkedFakeService.from_client(client, archive):
400
response = client.get_messages('baz', include_hidden=True)
401
self.assertMessageIDs(['bar', 'foo'], response['messages'])
402
response = client.get_messages('baz', include_hidden=False)
403
self.assertMessageIDs(['bar'], response['messages'])
405
def test_display_type_unknown_value(self):
406
client = GrackleClient('localhost', 8445)
407
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
408
with ForkedFakeService.from_client(client, archive):
409
with ExpectedException(UnsupportedDisplayType, ''):
410
client.get_messages('baz', display_type='unknown')
412
def test_display_type_headers_only(self):
413
client = GrackleClient('localhost', 8446)
416
make_message('foo', body=u'abcdefghi',
417
headers={'From': 'me', 'To': 'you'})]}
418
with ForkedFakeService.from_client(client, archive):
419
response = client.get_messages('baz', display_type='headers-only')
420
first_message = response['messages'][0]
421
self.assertEqual('foo', first_message['message_id'])
423
archive['baz'][0]['headers'], first_message['headers'])
424
self.assertNotIn('body', first_message)
426
def test_display_type_text_only(self):
427
client = GrackleClient('localhost', 8446)
432
headers={'From': 'me', 'To': 'you'},
433
attachment_type='text/x-diff')]}
434
with ForkedFakeService.from_client(client, archive):
435
response = client.get_messages('baz', display_type='text-only')
436
first_message = response['messages'][0]
437
self.assertEqual('foo', first_message['message_id'])
438
self.assertEqual('me', first_message['headers']['From'])
439
self.assertEqual('you', first_message['headers']['To'])
440
self.assertEqual(archive['baz'][0]['body'], first_message['body'])
442
def test_display_type_all(self):
443
client = GrackleClient('localhost', 8447)
448
headers={'From': 'me', 'To': 'you'},
449
attachment_type='text/x-diff')]}
450
with ForkedFakeService.from_client(client, archive):
451
response = client.get_messages('baz', display_type='all')
452
first_message = response['messages'][0]
453
self.assertEqual('foo', first_message['message_id'])
454
self.assertEqual('me', first_message['headers']['From'])
455
self.assertEqual('you', first_message['headers']['To'])
456
self.assertEqual(archive['baz'][0]['body'], first_message['body'])
458
def test_date_range(self):
459
client = GrackleClient('localhost', 8448)
463
'foo', 'abcdefghi', headers={'date': '2011-12-31'}),
465
'bar', 'abcdefghi', headers={'date': '2012-01-01'}),
467
'qux', 'abcdefghi', headers={'date': '2012-01-15'}),
469
'naf', 'abcdefghi', headers={'date': '2012-01-31'}),
471
'doh', 'abcdefghi', headers={'date': '2012-02-01'}),
473
with ForkedFakeService.from_client(client, archive):
474
response = client.get_messages(
475
'baz', date_range='2012-01-01..2012-01-31')
476
ids = sorted(m['message_id'] for m in response['messages'])
477
self.assertEqual(['bar', 'naf', 'qux'], ids)
479
def test_date_range_unparsabledaterange(self):
480
client = GrackleClient('localhost', 8449)
481
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
482
with ForkedFakeService.from_client(client, archive):
483
with ExpectedException(UnparsableDateRange, ''):
484
client.get_messages('baz', date_range='2012-01-01')
486
def test_date_range_unparsabledaterange_missing_part(self):
487
client = GrackleClient('localhost', 8450)
488
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
489
with ForkedFakeService.from_client(client, archive):
490
with ExpectedException(UnparsableDateRange, ''):
491
client.get_messages('baz', date_range='2012-01-01..')
493
def test_date_range_unparsabledaterange_extra_part(self):
494
client = GrackleClient('localhost', 8451)
495
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
496
with ForkedFakeService.from_client(client, archive):
497
with ExpectedException(UnparsableDateRange, ''):
498
client.get_messages('baz', date_range='2012-01..12-02..12-03')
501
class TestHideMessages(TestCase):
503
def test_hide_message_true(self):
504
client = GrackleClient('localhost', 8470)
507
make_message('foo', hidden=False),
509
with ForkedFakeService.from_client(client, archive):
510
response = client.hide_message('baz', 'foo', hidden=True)
511
self.assertEqual('foo', response['message_id'])
512
self.assertIs(True, response['hidden'])
514
def test_hide_message_false(self):
515
client = GrackleClient('localhost', 8470)
518
make_message('foo', hidden=True),
520
with ForkedFakeService.from_client(client, archive):
521
response = client.hide_message('baz', 'foo', hidden=False)
522
self.assertEqual('foo', response['message_id'])
523
self.assertIs(False, response['hidden'])