12
15
from unittest import TestCase
13
16
from urlparse import urlparse
14
from urlparse import parse_qs
16
18
from testtools import ExpectedException
18
20
from grackle.client import (
23
UnsupportedDisplayType,
24
def threaded_messages(messages):
28
for message in messages:
29
if message.get('in_reply_to') is None:
30
threads[message['message_id']] = [message]
33
pending.append(message)
34
for message in pending:
35
threads[message['in_reply_to']].append(message)
36
return threads.values()
41
def __init__(self, messages):
42
self.messages = messages
44
def get_messages(self, archive_id, query_string):
45
query = parse_qs(query_string)
46
parameters = simplejson.loads(query['parameters'][0])
47
order = parameters.get('order')
48
messages = self.messages[archive_id]
49
if order is not None :
50
if order not in SUPPORTED_ORDERS:
51
raise UnsupportedOrder
52
elif order.startswith('thread_'):
53
threaded = threaded_messages(messages)
55
if order == 'thread_subject':
56
threaded.sort(key=lambda t: t[0]['subject'])
57
if order == 'thread_oldest':
58
threaded.sort(key=lambda t: min(m['date'] for m in t))
59
if order == 'thread_newest':
60
threaded.sort(key=lambda t: max(m['date'] for m in t))
61
for thread in threaded:
62
messages.extend(thread)
64
messages.sort(key=lambda m: m[order])
66
for message in messages:
68
not parameters['include_hidden']
69
and message.get('hidden', False)):
72
if ('message_ids' in parameters and
73
message['message_id'] not in parameters['message_ids']):
75
message = dict(message)
76
if 'headers' in parameters:
78
(k, v) for k, v in message['headers'].iteritems()
79
if k in parameters['headers'])
80
message['headers'] = headers
81
max_body = parameters.get('max_body_length')
82
if max_body is not None:
83
message['body'] = message['body'][:max_body]
84
new_messages.append(message)
85
messages = new_messages
86
limit = parameters.get('limit', 100)
87
memo = parameters.get('memo')
88
message_id_indices = dict(
89
(m['message_id'], idx) for idx, m in enumerate(messages))
93
start = message_id_indices[memo.encode('rot13')]
95
previous_memo = messages[start - 1]['message_id'].encode('rot13')
98
end = min(start + limit, len(messages))
99
if end < len(messages):
100
next_memo = messages[end]['message_id'].encode('rot13')
103
messages = messages[start:end]
106
'messages': messages,
107
'next_memo': next_memo,
108
'previous_memo': previous_memo
116
def __init__(self, port, messages=None, write_logs=False):
26
from grackle.store import (
32
def make_message(message_id, body='body', headers=None, hidden=False):
35
headers['Message-Id'] = message_id
37
'message_id': message_id,
39
'thread_id': message_id,
40
'date': headers.get('date', '2005-01-01'),
41
'subject': headers.get('subject', 'subject'),
42
'author': headers.get('author', 'author'),
45
'replies': headers.get('in-reply-to', None),
51
def make_mime_message(message_id, body='body', headers=None, hidden=False,
52
attachment_type=None):
55
parts = MIMEMultipart()
56
parts.attach(MIMEText(body))
57
if attachment_type is not None:
58
attachment = Message()
59
attachment.set_payload('attactment data.')
60
attachment['Content-Type'] = attachment_type
61
attachment['Content-Disposition'] = 'attachment; filename="file.ext"'
62
parts.attach(attachment)
64
message.set_payload(parts.as_string())
65
for key, value in headers.items():
67
return make_json_message(message_id, message.as_string())
70
class ForkedFakeService:
71
"""A Grackle service fake, as a ContextManager."""
73
def __init__(self, port, message_archives=None, write_logs=False):
76
:param port: The tcp port to use.
77
:param message_archives: A dict of lists of dicts representing
78
archives of messages. The outer dict represents the archive,
79
the list represents the list of messages for that archive.
80
:param write_logs: If true, log messages will be written to stdout.
84
if message_archives is None:
85
self.message_archives = {}
122
self.messages = messages
87
self.message_archives = message_archives
123
88
self.read_end, self.write_end = os.pipe()
124
89
self.write_logs = write_logs
127
def from_client(client, messages=None):
128
return ForkedFake(client.port, messages)
92
def from_client(client, message_archives=None):
93
"""Instantiate a ForkedFakeService from the client.
95
:param port: The client to provide service for.
96
:param message_archives: A dict of lists of dicts representing
97
archives of messages. The outer dict represents the archive,
98
the list represents the list of messages for that archive.
100
return ForkedFakeService(client.port, message_archives)
130
102
def is_ready(self):
103
"""Tell the parent process that the server is ready for writes."""
131
104
os.write(self.write_end, 'asdf')
133
106
def __enter__(self):
109
Fork and start a server in the child. Return when the server is ready
136
113
self.start_server()
154
132
os.kill(self.pid, SIGKILL)
157
SUPPORTED_ORDERS = set(
158
['date', 'author', 'subject', 'thread_newest', 'thread_oldest',
162
135
class FakeGrackleRequestHandler(BaseHTTPRequestHandler):
136
"""A request handler that forwards to server.store."""
164
138
def __init__(self, *args, **kwargs):
139
"""Constructor. Sets up logging."""
165
140
self.logger = logging.getLogger('http')
166
141
BaseHTTPRequestHandler.__init__(self, *args, **kwargs)
168
143
def do_POST(self):
144
"""Create a message on POST."""
169
145
message = self.rfile.read(int(self.headers['content-length']))
170
if message == 'This is a message':
171
self.send_response(httplib.CREATED)
175
self.send_error(httplib.BAD_REQUEST)
146
scheme, netloc, path, params, query_string, fragments = (
148
parts = path.split('/')
149
if parts[1] == 'archive' and len(parts) == 4:
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)
177
159
def do_GET(self):
160
"""Retrieve a list of messages on GET."""
178
161
scheme, netloc, path, params, query_string, fragments = (
179
162
urlparse(self.path))
180
163
parts = path.split('/')
185
168
self.send_response(httplib.OK)
186
169
self.end_headers()
187
170
self.wfile.write(simplejson.dumps(response))
188
except UnsupportedOrder:
189
self.send_response(httplib.BAD_REQUEST)
190
self.wfile.write('Unsupported order')
171
except Exception, error:
173
httplib.BAD_REQUEST, error.__doc__)
193
176
def log_message(self, format, *args):
177
"""Override log_message to use standard Python logging."""
194
178
message = "%s - - [%s] %s\n" % (
195
self.address_string(), self.log_date_time_string(), format%args)
179
self.address_string(), self.log_date_time_string(), format % args)
196
180
self.logger.info(message)
199
183
class TestPutMessage(TestCase):
201
185
def test_put_message(self):
202
client = GrackleClient('localhost', 8436)
203
with ForkedFake.from_client(client):
204
client.put_message('arch1', 'asdf', StringIO('This is a message'))
186
client = GrackleClient('localhost', 8420)
187
message_archives = {'arch1': []}
188
with ForkedFakeService.from_client(client, message_archives):
189
client.put_message('arch1', 'id1', StringIO('This is a message'))
190
response = client.get_messages('arch1')
191
self.assertEqual(1, len(response['messages']))
192
message = response['messages'][0]
193
self.assertEqual('id1', message['message_id'])
195
def test_put_message_without_archive(self):
196
client = GrackleClient('localhost', 8421)
197
message_archives = {'arch1': []}
198
with ForkedFakeService.from_client(client, message_archives):
205
199
with ExpectedException(Exception, 'wtf'):
206
client.put_message('arch1', 'asdf',
207
StringIO('This is not a message'))
200
client.put_message('no-archive', 'id1', StringIO('message'))
210
203
class TestGetMessages(TestCase):
231
223
def test_get_messages_by_id(self):
232
224
client = GrackleClient('localhost', 8437)
233
with ForkedFake.from_client(client,
235
[{'message_id': 'foo'},
236
{'message_id': 'bar'}]}):
226
'baz': [make_message('foo'), make_message('bar')]}
227
with ForkedFakeService.from_client(client, archive):
237
228
response = client.get_messages('baz', message_ids=['foo'])
238
229
message, = response['messages']
239
230
self.assertEqual('foo', message['message_id'])
241
232
def test_get_messages_batching(self):
242
233
client = GrackleClient('localhost', 8438)
243
with ForkedFake.from_client(client,
245
[{'message_id': 'foo'},
246
{'message_id': 'bar'}]}):
234
archive = {'baz': [make_message('foo'), make_message('bar')]}
235
with ForkedFakeService.from_client(client, archive):
247
236
response = client.get_messages('baz', limit=1)
248
237
self.assertEqual(1, len(response['messages']))
249
238
messages = response['messages']
303
301
def test_get_messages_thread_newest_order(self):
304
302
client = GrackleClient('localhost', 8439)
305
with ForkedFake.from_client(client, {'baz': [
306
{'message_id': 'bar', 'date': 'x'},
307
{'message_id': 'qux', 'date': 'w'},
308
{'message_id': 'foo', 'date': 'y', 'in_reply_to': 'bar'},
309
{'message_id': 'baz', 'date': 'z', 'in_reply_to': 'qux'},
305
make_message('bar', headers={'date': 'x'}),
306
make_message('qux', headers={'date': 'w'}),
307
make_message('foo', headers={'date': 'y',
308
'in-reply-to': 'bar'}),
309
make_message('baz', headers={'date': 'z',
310
'in-reply-to': 'qux'}),
312
with ForkedFakeService.from_client(client, archive):
311
313
response = client.get_messages('baz', order='date')
312
314
self.assertIDOrder(
313
315
['qux', 'bar', 'foo', 'baz'], response['messages'])
318
320
def test_get_messages_unsupported_order(self):
319
321
client = GrackleClient('localhost', 8439)
320
with ForkedFake.from_client(client,
321
{'baz': [{'message_id': 'foo', 'date': '2011-03-25'},
322
{'message_id': 'bar', 'date': '2011-03-24'}]}):
323
with ExpectedException(UnsupportedOrder):
324
make_message('foo', headers={'date': '2011-03-25'}),
325
make_message('foo', headers={'date': '2011-03-24'}),
327
with ForkedFakeService.from_client(client, archive):
328
with ExpectedException(UnsupportedOrder, ''):
324
329
client.get_messages('baz', order='nonsense')
326
331
def test_get_messages_headers_no_headers(self):
327
332
client = GrackleClient('localhost', 8440)
328
with ForkedFake.from_client(client,
330
{'message_id': 'foo'}
333
archive = {'baz': [make_message('foo')]}
334
with ForkedFakeService.from_client(client, archive):
332
335
response = client.get_messages('baz', headers=[
333
336
'Subject', 'Date', 'X-Launchpad-Message-Rationale'])
334
337
first_message = response['messages'][0]
362
364
def test_get_messages_max_body_length(self):
363
365
client = GrackleClient('localhost', 8443)
364
with ForkedFake.from_client(client,
366
{'message_id': 'foo', 'body': u'abcdefghi'}
366
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
367
with ForkedFakeService.from_client(client, archive):
368
368
response = client.get_messages('baz', max_body_length=3)
369
369
first_message = response['messages'][0]
370
370
self.assertEqual('abc', first_message['body'])
372
372
def test_include_hidden(self):
373
373
client = GrackleClient('localhost', 8444)
374
with ForkedFake.from_client(client,
376
{'message_id': 'foo', 'hidden': True},
377
{'message_id': 'bar', 'hidden': False}
376
make_message('foo', hidden=True),
377
make_message('bar', hidden=False),
379
with ForkedFakeService.from_client(client, archive):
379
380
response = client.get_messages('baz', include_hidden=True)
380
381
self.assertMessageIDs(['bar', 'foo'], response['messages'])
381
382
response = client.get_messages('baz', include_hidden=False)
382
383
self.assertMessageIDs(['bar'], response['messages'])
385
def test_display_type_unknown_value(self):
386
client = GrackleClient('localhost', 8445)
387
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
388
with ForkedFakeService.from_client(client, archive):
389
with ExpectedException(UnsupportedDisplayType, ''):
390
client.get_messages('baz', display_type='unknown')
392
def test_display_type_headers_only(self):
393
client = GrackleClient('localhost', 8446)
396
make_message('foo', body=u'abcdefghi',
397
headers={'From': 'me', 'To': 'you'})]}
398
with ForkedFakeService.from_client(client, archive):
399
response = client.get_messages('baz', display_type='headers-only')
400
first_message = response['messages'][0]
401
self.assertEqual('foo', first_message['message_id'])
403
{'From': 'me', 'Message-Id': 'foo', 'To': 'you'},
404
first_message['headers'])
405
self.assertNotIn('body', first_message)
407
def test_display_type_text_only(self):
408
client = GrackleClient('localhost', 8446)
413
headers={'From': 'me', 'To': 'you'},
414
attachment_type='text/x-diff')]}
415
with ForkedFakeService.from_client(client, archive):
416
response = client.get_messages('baz', display_type='text-only')
417
first_message = response['messages'][0]
418
self.assertEqual('foo', first_message['message_id'])
419
self.assertEqual('me', first_message['headers']['From'])
420
self.assertEqual('you', first_message['headers']['To'])
421
self.assertEqual(archive['baz'][0]['body'], first_message['body'])
423
def test_display_type_all(self):
424
client = GrackleClient('localhost', 8447)
429
headers={'From': 'me', 'To': 'you'},
430
attachment_type='text/x-diff')]}
431
with ForkedFakeService.from_client(client, archive):
432
response = client.get_messages('baz', display_type='all')
433
first_message = response['messages'][0]
434
self.assertEqual('foo', first_message['message_id'])
435
self.assertEqual('me', first_message['headers']['From'])
436
self.assertEqual('you', first_message['headers']['To'])
437
self.assertEqual(archive['baz'][0]['body'], first_message['body'])
439
def test_date_range(self):
440
client = GrackleClient('localhost', 8448)
444
'foo', 'abcdefghi', headers={'date': '2011-12-31'}),
446
'bar', 'abcdefghi', headers={'date': '2012-01-01'}),
448
'qux', 'abcdefghi', headers={'date': '2012-01-15'}),
450
'naf', 'abcdefghi', headers={'date': '2012-01-31'}),
452
'doh', 'abcdefghi', headers={'date': '2012-02-01'}),
454
with ForkedFakeService.from_client(client, archive):
455
response = client.get_messages(
456
'baz', date_range='2012-01-01..2012-01-31')
457
ids = sorted(m['message_id'] for m in response['messages'])
458
self.assertEqual(['bar', 'naf', 'qux'], ids)
460
def test_date_range_unparsabledaterange(self):
461
client = GrackleClient('localhost', 8449)
462
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
463
with ForkedFakeService.from_client(client, archive):
464
with ExpectedException(UnparsableDateRange, ''):
465
client.get_messages('baz', date_range='2012-01-01')
467
def test_date_range_unparsabledaterange_missing_part(self):
468
client = GrackleClient('localhost', 8450)
469
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
470
with ForkedFakeService.from_client(client, archive):
471
with ExpectedException(UnparsableDateRange, ''):
472
client.get_messages('baz', date_range='2012-01-01..')
474
def test_date_range_unparsabledaterange_extra_part(self):
475
client = GrackleClient('localhost', 8451)
476
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
477
with ForkedFakeService.from_client(client, archive):
478
with ExpectedException(UnparsableDateRange, ''):
479
client.get_messages('baz', date_range='2012-01..12-02..12-03')