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
59
for archive_id, messages in service.messages.iteritems():
114
service.store = MemoryStore(self.message_archives)
115
for archive_id, messages in service.store.message_archives.iteritems():
60
116
for message in messages:
61
117
message.setdefault('headers', {})
121
stream=sys.stderr, level=logging.INFO)
63
122
service.serve_forever()
65
124
def __exit__(self, exc_type, exc_val, traceback):
66
125
os.kill(self.pid, SIGKILL)
69
SUPPORTED_ORDERS = set(
70
['date', 'author', 'subject', 'thread_newest', 'thread_oldest',
74
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)
76
136
def do_POST(self):
77
message = self.rfile.read(int(self.headers['content-length']))
78
if message == 'This is a message':
79
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'
83
self.send_error(httplib.BAD_REQUEST)
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."""
86
171
scheme, netloc, path, params, query_string, fragments = (
87
172
urlparse(self.path))
88
173
parts = path.split('/')
89
174
if parts[1] == 'archive':
90
self.get_messages(parts[2], query_string)
92
def get_messages(self, archive_id, query_string):
93
query = parse_qs(query_string)
94
parameters = simplejson.loads(query['parameters'][0])
95
order = parameters.get('order')
96
messages = self.server.messages[archive_id]
97
if order is not None :
98
if order not in SUPPORTED_ORDERS:
99
self.send_response(httplib.BAD_REQUEST)
100
self.wfile.write('Unsupported order')
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__)
102
elif order.startswith('thread_'):
103
threaded = threaded_messages(messages)
105
if order == 'thread_subject':
106
threaded.sort(key=lambda t: t[0]['subject'])
107
if order == 'thread_oldest':
108
threaded.sort(key=lambda t: min(m['date'] for m in t))
109
if order == 'thread_newest':
110
threaded.sort(key=lambda t: max(m['date'] for m in t))
111
for thread in threaded:
112
messages.extend(thread)
114
messages.sort(key=lambda m: m[order])
115
messages = [m for m in messages
116
if 'message_ids' not in parameters or
117
m['message_id'] in parameters['message_ids']]
118
self.send_response(httplib.OK)
120
limit = parameters.get('limit', 100)
121
memo = parameters.get('memo')
122
message_id_indices = dict(
123
(m['message_id'], idx) for idx, m in enumerate(messages))
127
start = message_id_indices[memo.encode('rot13')]
129
previous_memo = messages[start - 1]['message_id'].encode('rot13')
132
end = min(start + limit, len(messages))
133
if end < len(messages):
134
next_memo = messages[end]['message_id'].encode('rot13')
137
messages = messages[start:end]
139
for message in messages:
140
message = dict(message)
141
if 'headers' in parameters:
143
(k, v) for k, v in message['headers'].iteritems()
144
if k in parameters['headers'])
145
message['headers'] = headers
146
new_messages.append(message)
148
'messages': new_messages,
149
'next_memo': next_memo,
150
'previous_memo': previous_memo
152
self.wfile.write(simplejson.dumps(response))
155
def fake_grackle_service(client, messages=None):
158
return ForkedFake(client.port, messages)
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)
161
193
class TestPutMessage(TestCase):
163
195
def test_put_message(self):
164
client = GrackleClient('localhost', 8436)
165
with fake_grackle_service(client):
166
client.put_message('arch1', 'asdf', 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):
167
209
with ExpectedException(Exception, 'wtf'):
168
client.put_message('arch1', 'asdf',
169
StringIO('This is not a message'))
210
client.put_message('no-archive', 'id1', StringIO('message'))
172
213
class TestGetMessages(TestCase):
310
364
self.assertEqual({}, first_message['headers'])
312
366
def test_get_messages_headers_include_headers(self):
313
client = GrackleClient('localhost', 8440)
314
with fake_grackle_service(client,
316
{'message_id': 'foo', 'headers': {'From': 'me', 'To': 'you'}}
367
client = GrackleClient('localhost', 8442)
370
make_message('foo', headers={'From': 'me', 'To': 'you'})]}
371
with ForkedFakeService.from_client(client, archive):
318
372
response = client.get_messages('baz', headers=[
320
374
first_message = response['messages'][0]
321
375
self.assertEqual('foo', first_message['message_id'])
322
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'])