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):
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.
77
if message_archives is None:
78
self.message_archives = {}
122
self.messages = messages
80
self.message_archives = message_archives
123
81
self.read_end, self.write_end = os.pipe()
124
82
self.write_logs = write_logs
127
def from_client(client, messages=None):
128
return ForkedFake(client.port, messages)
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)
130
95
def is_ready(self):
96
"""Tell the parent process that the server is ready for writes."""
131
97
os.write(self.write_end, 'asdf')
133
99
def __enter__(self):
102
Fork and start a server in the child. Return when the server is ready
136
106
self.start_server()
154
125
os.kill(self.pid, SIGKILL)
157
SUPPORTED_ORDERS = set(
158
['date', 'author', 'subject', 'thread_newest', 'thread_oldest',
162
128
class FakeGrackleRequestHandler(BaseHTTPRequestHandler):
129
"""A request handler that forwards to server.store."""
164
131
def __init__(self, *args, **kwargs):
132
"""Constructor. Sets up logging."""
165
133
self.logger = logging.getLogger('http')
166
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.
147
self.server.store.put_archive(parts[2])
148
self.send_response(httplib.CREATED)
152
self.send_error(httplib.BAD_REQUEST)
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)
168
164
def do_POST(self):
169
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)
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)
177
184
def do_GET(self):
185
"""Retrieve a list of messages on GET."""
178
186
scheme, netloc, path, params, query_string, fragments = (
179
187
urlparse(self.path))
180
188
parts = path.split('/')
185
193
self.send_response(httplib.OK)
186
194
self.end_headers()
187
195
self.wfile.write(simplejson.dumps(response))
188
except UnsupportedOrder:
189
self.send_response(httplib.BAD_REQUEST)
190
self.wfile.write('Unsupported order')
196
except Exception, error:
198
httplib.BAD_REQUEST, error.__doc__)
193
201
def log_message(self, format, *args):
202
"""Override log_message to use standard Python logging."""
194
203
message = "%s - - [%s] %s\n" % (
195
self.address_string(), self.log_date_time_string(), format%args)
204
self.address_string(), self.log_date_time_string(), format % args)
196
205
self.logger.info(message)
208
class TestPutArchive(TestCase):
210
def test_put_message(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']))
199
219
class TestPutMessage(TestCase):
201
221
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'))
222
client = GrackleClient('localhost', 8420)
223
message_archives = {'arch1': []}
224
with ForkedFakeService.from_client(client, message_archives):
225
client.put_message('arch1', 'id1', StringIO('This is a message'))
226
response = client.get_messages('arch1')
227
self.assertEqual(1, len(response['messages']))
228
message = response['messages'][0]
229
self.assertEqual('id1', message['message_id'])
231
def test_put_message_without_archive(self):
232
client = GrackleClient('localhost', 8421)
233
message_archives = {'arch1': []}
234
with ForkedFakeService.from_client(client, message_archives):
205
235
with ExpectedException(Exception, 'wtf'):
206
client.put_message('arch1', 'asdf',
207
StringIO('This is not a message'))
236
client.put_message('no-archive', 'id1', StringIO('message'))
210
239
class TestGetMessages(TestCase):
318
360
def test_get_messages_unsupported_order(self):
319
361
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):
364
make_message('foo', headers={'date': '2011-03-25'}),
365
make_message('foo', headers={'date': '2011-03-24'}),
367
with ForkedFakeService.from_client(client, archive):
368
with ExpectedException(UnsupportedOrder, ''):
324
369
client.get_messages('baz', order='nonsense')
326
371
def test_get_messages_headers_no_headers(self):
327
372
client = GrackleClient('localhost', 8440)
328
with ForkedFake.from_client(client,
330
{'message_id': 'foo'}
373
archive = {'baz': [make_message('foo')]}
374
with ForkedFakeService.from_client(client, archive):
332
375
response = client.get_messages('baz', headers=[
333
376
'Subject', 'Date', 'X-Launchpad-Message-Rationale'])
334
377
first_message = response['messages'][0]
362
404
def test_get_messages_max_body_length(self):
363
405
client = GrackleClient('localhost', 8443)
364
with ForkedFake.from_client(client,
366
{'message_id': 'foo', 'body': u'abcdefghi'}
406
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
407
with ForkedFakeService.from_client(client, archive):
368
408
response = client.get_messages('baz', max_body_length=3)
369
409
first_message = response['messages'][0]
370
410
self.assertEqual('abc', first_message['body'])
372
412
def test_include_hidden(self):
373
413
client = GrackleClient('localhost', 8444)
374
with ForkedFake.from_client(client,
376
{'message_id': 'foo', 'hidden': True},
377
{'message_id': 'bar', 'hidden': False}
416
make_message('foo', hidden=True),
417
make_message('bar', hidden=False),
419
with ForkedFakeService.from_client(client, archive):
379
420
response = client.get_messages('baz', include_hidden=True)
380
421
self.assertMessageIDs(['bar', 'foo'], response['messages'])
381
422
response = client.get_messages('baz', include_hidden=False)
382
423
self.assertMessageIDs(['bar'], response['messages'])
425
def test_display_type_unknown_value(self):
426
client = GrackleClient('localhost', 8445)
427
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
428
with ForkedFakeService.from_client(client, archive):
429
with ExpectedException(UnsupportedDisplayType, ''):
430
client.get_messages('baz', display_type='unknown')
432
def test_display_type_headers_only(self):
433
client = GrackleClient('localhost', 8446)
436
make_message('foo', body=u'abcdefghi',
437
headers={'From': 'me', 'To': 'you'})]}
438
with ForkedFakeService.from_client(client, archive):
439
response = client.get_messages('baz', display_type='headers-only')
440
first_message = response['messages'][0]
441
self.assertEqual('foo', first_message['message_id'])
443
archive['baz'][0]['headers'], first_message['headers'])
444
self.assertNotIn('body', first_message)
446
def test_display_type_text_only(self):
447
client = GrackleClient('localhost', 8446)
452
headers={'From': 'me', 'To': 'you'},
453
attachment_type='text/x-diff')]}
454
with ForkedFakeService.from_client(client, archive):
455
response = client.get_messages('baz', display_type='text-only')
456
first_message = response['messages'][0]
457
self.assertEqual('foo', first_message['message_id'])
458
self.assertEqual('me', first_message['headers']['From'])
459
self.assertEqual('you', first_message['headers']['To'])
460
self.assertEqual(archive['baz'][0]['body'], first_message['body'])
462
def test_display_type_all(self):
463
client = GrackleClient('localhost', 8447)
468
headers={'From': 'me', 'To': 'you'},
469
attachment_type='text/x-diff')]}
470
with ForkedFakeService.from_client(client, archive):
471
response = client.get_messages('baz', display_type='all')
472
first_message = response['messages'][0]
473
self.assertEqual('foo', first_message['message_id'])
474
self.assertEqual('me', first_message['headers']['From'])
475
self.assertEqual('you', first_message['headers']['To'])
476
self.assertEqual(archive['baz'][0]['body'], first_message['body'])
478
def test_date_range(self):
479
client = GrackleClient('localhost', 8448)
483
'foo', 'abcdefghi', headers={'date': '2011-12-31'}),
485
'bar', 'abcdefghi', headers={'date': '2012-01-01'}),
487
'qux', 'abcdefghi', headers={'date': '2012-01-15'}),
489
'naf', 'abcdefghi', headers={'date': '2012-01-31'}),
491
'doh', 'abcdefghi', headers={'date': '2012-02-01'}),
493
with ForkedFakeService.from_client(client, archive):
494
response = client.get_messages(
495
'baz', date_range='2012-01-01..2012-01-31')
496
ids = sorted(m['message_id'] for m in response['messages'])
497
self.assertEqual(['bar', 'naf', 'qux'], ids)
499
def test_date_range_unparsabledaterange(self):
500
client = GrackleClient('localhost', 8449)
501
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
502
with ForkedFakeService.from_client(client, archive):
503
with ExpectedException(UnparsableDateRange, ''):
504
client.get_messages('baz', date_range='2012-01-01')
506
def test_date_range_unparsabledaterange_missing_part(self):
507
client = GrackleClient('localhost', 8450)
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_extra_part(self):
514
client = GrackleClient('localhost', 8451)
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..12-02..12-03')
521
class TestHideMessages(TestCase):
523
def test_hide_message_true(self):
524
client = GrackleClient('localhost', 8470)
527
make_message('foo', hidden=False),
529
with ForkedFakeService.from_client(client, archive):
530
response = client.hide_message('baz', 'foo', hidden=True)
531
self.assertEqual('foo', response['message_id'])
532
self.assertIs(True, response['hidden'])
534
def test_hide_message_false(self):
535
client = GrackleClient('localhost', 8470)
538
make_message('foo', hidden=True),
540
with ForkedFakeService.from_client(client, archive):
541
response = client.hide_message('baz', 'foo', hidden=False)
542
self.assertEqual('foo', response['message_id'])
543
self.assertIs(False, response['hidden'])