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
20
from grackle.client import (
23
UnsupportedDisplayType,
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):
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.
42
self.messages = messages
77
if message_archives is None:
78
self.message_archives = {}
80
self.message_archives = message_archives
43
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)
45
95
def is_ready(self):
96
"""Tell the parent process that the server is ready for writes."""
46
97
os.write(self.write_end, 'asdf')
48
99
def __enter__(self):
102
Fork and start a server in the child. Return when the server is ready
51
106
self.start_server()
56
111
def start_server(self):
112
"""Start the HTTP server."""
57
113
service = HTTPServer(('', self.port), FakeGrackleRequestHandler)
58
service.messages = self.messages
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)
60
122
service.serve_forever()
62
124
def __exit__(self, exc_type, exc_val, traceback):
63
125
os.kill(self.pid, SIGKILL)
66
SUPPORTED_ORDERS = set(
67
['date', 'author', 'subject', 'thread_newest', 'thread_oldest',
71
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)
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)
73
155
def do_POST(self):
74
message = self.rfile.read(int(self.headers['content-length']))
75
if message == 'This is a message':
76
self.send_response(httplib.CREATED)
80
self.send_error(httplib.BAD_REQUEST)
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)
176
"""Retrieve a list of messages on GET."""
83
177
scheme, netloc, path, params, query_string, fragments = (
84
178
urlparse(self.path))
85
archive = os.path.split(path)[1]
86
query = parse_qs(query_string)
87
parameters = simplejson.loads(query['parameters'][0])
88
order = parameters.get('order')
89
messages = self.server.messages[archive]
90
if order is not None :
91
if order not in SUPPORTED_ORDERS:
92
self.send_response(httplib.BAD_REQUEST)
93
self.wfile.write('Unsupported order')
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__)
95
elif order.startswith('thread_'):
96
threaded = threaded_messages(messages)
98
if order == 'thread_subject':
99
threaded.sort(key=lambda t: t[0]['subject'])
100
if order == 'thread_oldest':
101
threaded.sort(key=lambda t: min(m['date'] for m in t))
102
if order == 'thread_newest':
103
threaded.sort(key=lambda t: max(m['date'] for m in t))
104
for thread in threaded:
105
messages.extend(thread)
107
messages.sort(key=lambda m: m[order])
108
messages = [m for m in messages
109
if 'message_ids' not in parameters or
110
m['message_id'] in parameters['message_ids']]
111
self.send_response(httplib.OK)
113
limit = parameters.get('limit', 100)
114
memo = parameters.get('memo')
115
message_id_indices = dict(
116
(m['message_id'], idx) for idx, m in enumerate(messages))
120
start = message_id_indices[memo.encode('rot13')]
122
previous_memo = messages[start - 1]['message_id'].encode('rot13')
125
end = min(start + limit, len(messages))
126
if end < len(messages):
127
next_memo = messages[end]['message_id'].encode('rot13')
130
messages = messages[start:end]
132
'messages': messages,
133
'next_memo': next_memo,
134
'previous_memo': previous_memo
136
self.wfile.write(simplejson.dumps(response))
139
def fake_grackle_service(client, messages=None):
142
return ForkedFake(client.port, messages)
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)
145
199
class TestPutMessage(TestCase):
147
201
def test_put_message(self):
148
client = GrackleClient('localhost', 8436)
149
with fake_grackle_service(client):
150
client.put_message('arch1', 'asdf', StringIO('This is a message'))
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):
151
215
with ExpectedException(Exception, 'wtf'):
152
client.put_message('arch1', 'asdf',
153
StringIO('This is not a message'))
216
client.put_message('no-archive', 'id1', StringIO('message'))
156
219
class TestGetMessages(TestCase):
264
340
def test_get_messages_unsupported_order(self):
265
341
client = GrackleClient('localhost', 8439)
266
with fake_grackle_service(client,
267
{'baz': [{'message_id': 'foo', 'date': '2011-03-25'},
268
{'message_id': 'bar', 'date': '2011-03-24'}]}):
269
with ExpectedException(UnsupportedOrder):
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, ''):
270
349
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'])