15
12
from unittest import TestCase
16
13
from urlparse import urlparse
14
from urlparse import parse_qs
18
16
from testtools import ExpectedException
20
18
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.
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):
77
if message_archives is None:
78
self.message_archives = {}
80
self.message_archives = message_archives
122
self.messages = messages
81
123
self.read_end, self.write_end = os.pipe()
82
124
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)
127
def from_client(client, messages=None):
128
return ForkedFake(client.port, messages)
95
130
def is_ready(self):
96
"""Tell the parent process that the server is ready for writes."""
97
131
os.write(self.write_end, 'asdf')
99
133
def __enter__(self):
102
Fork and start a server in the child. Return when the server is ready
106
136
self.start_server()
125
154
os.kill(self.pid, SIGKILL)
157
SUPPORTED_ORDERS = set(
158
['date', 'author', 'subject', 'thread_newest', 'thread_oldest',
128
162
class FakeGrackleRequestHandler(BaseHTTPRequestHandler):
129
"""A request handler that forwards to server.store."""
131
164
def __init__(self, *args, **kwargs):
132
"""Constructor. Sets up logging."""
133
165
self.logger = logging.getLogger('http')
134
166
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)
164
168
def do_POST(self):
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)
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)
184
177
def do_GET(self):
185
"""Retrieve a list of messages on GET."""
186
178
scheme, netloc, path, params, query_string, fragments = (
187
179
urlparse(self.path))
188
180
parts = path.split('/')
193
185
self.send_response(httplib.OK)
194
186
self.end_headers()
195
187
self.wfile.write(simplejson.dumps(response))
196
except Exception, error:
198
httplib.BAD_REQUEST, error.__doc__)
188
except UnsupportedOrder:
189
self.send_response(httplib.BAD_REQUEST)
190
self.wfile.write('Unsupported order')
201
193
def log_message(self, format, *args):
202
"""Override log_message to use standard Python logging."""
203
194
message = "%s - - [%s] %s\n" % (
204
self.address_string(), self.log_date_time_string(), format % args)
195
self.address_string(), self.log_date_time_string(), format%args)
205
196
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']))
219
199
class TestPutMessage(TestCase):
221
201
def test_put_message(self):
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):
202
client = GrackleClient('localhost', 8436)
203
with ForkedFake.from_client(client):
204
client.put_message('arch1', 'asdf', StringIO('This is a message'))
235
205
with ExpectedException(Exception, 'wtf'):
236
client.put_message('no-archive', 'id1', StringIO('message'))
206
client.put_message('arch1', 'asdf',
207
StringIO('This is not a message'))
239
210
class TestGetMessages(TestCase):
360
318
def test_get_messages_unsupported_order(self):
361
319
client = GrackleClient('localhost', 8439)
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, ''):
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):
369
324
client.get_messages('baz', order='nonsense')
371
326
def test_get_messages_headers_no_headers(self):
372
327
client = GrackleClient('localhost', 8440)
373
archive = {'baz': [make_message('foo')]}
374
with ForkedFakeService.from_client(client, archive):
328
with ForkedFake.from_client(client,
330
{'message_id': 'foo'}
375
332
response = client.get_messages('baz', headers=[
376
333
'Subject', 'Date', 'X-Launchpad-Message-Rationale'])
377
334
first_message = response['messages'][0]
404
362
def test_get_messages_max_body_length(self):
405
363
client = GrackleClient('localhost', 8443)
406
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
407
with ForkedFakeService.from_client(client, archive):
364
with ForkedFake.from_client(client,
366
{'message_id': 'foo', 'body': u'abcdefghi'}
408
368
response = client.get_messages('baz', max_body_length=3)
409
369
first_message = response['messages'][0]
410
370
self.assertEqual('abc', first_message['body'])
412
372
def test_include_hidden(self):
413
373
client = GrackleClient('localhost', 8444)
416
make_message('foo', hidden=True),
417
make_message('bar', hidden=False),
419
with ForkedFakeService.from_client(client, archive):
374
with ForkedFake.from_client(client,
376
{'message_id': 'foo', 'hidden': True},
377
{'message_id': 'bar', 'hidden': False}
420
379
response = client.get_messages('baz', include_hidden=True)
421
380
self.assertMessageIDs(['bar', 'foo'], response['messages'])
422
381
response = client.get_messages('baz', include_hidden=False)
423
382
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'])