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):
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
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)
126
def from_client(client, messages=None):
127
return ForkedFake(client.port, messages)
95
129
def is_ready(self):
96
"""Tell the parent process that the server is ready for writes."""
97
130
os.write(self.write_end, 'asdf')
99
132
def __enter__(self):
102
Fork and start a server in the child. Return when the server is ready
106
135
self.start_server()
111
140
def start_server(self):
112
"""Start the HTTP server."""
113
141
service = HTTPServer(('', self.port), FakeGrackleRequestHandler)
114
service.store = MemoryStore(self.message_archives)
115
for archive_id, messages in service.store.message_archives.iteritems():
142
service.store = GrackleStore(self.messages)
143
for archive_id, messages in service.store.messages.iteritems():
116
144
for message in messages:
117
145
message.setdefault('headers', {})
121
stream=sys.stderr, level=logging.INFO)
147
# logging.basicConfig(
148
# stream=sys.stderr, level=logging.INFO)
122
149
service.serve_forever()
124
151
def __exit__(self, exc_type, exc_val, traceback):
125
152
os.kill(self.pid, SIGKILL)
155
SUPPORTED_ORDERS = set(
156
['date', 'author', 'subject', 'thread_newest', 'thread_oldest',
128
160
class FakeGrackleRequestHandler(BaseHTTPRequestHandler):
129
"""A request handler that forwards to server.store."""
131
162
def __init__(self, *args, **kwargs):
132
"""Constructor. Sets up logging."""
133
163
self.logger = logging.getLogger('http')
134
164
BaseHTTPRequestHandler.__init__(self, *args, **kwargs)
136
166
def do_POST(self):
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'
167
message = self.rfile.read(int(self.headers['content-length']))
168
if message == 'This is a message':
169
self.send_response(httplib.CREATED)
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)
173
self.send_error(httplib.BAD_REQUEST)
169
175
def do_GET(self):
170
"""Retrieve a list of messages on GET."""
171
176
scheme, netloc, path, params, query_string, fragments = (
172
177
urlparse(self.path))
173
178
parts = path.split('/')
178
183
self.send_response(httplib.OK)
179
184
self.end_headers()
180
185
self.wfile.write(simplejson.dumps(response))
181
except Exception, error:
183
httplib.BAD_REQUEST, error.__doc__)
186
except UnsupportedOrder:
187
self.send_response(httplib.BAD_REQUEST)
188
self.wfile.write('Unsupported order')
186
191
def log_message(self, format, *args):
187
"""Override log_message to use standard Python logging."""
188
192
message = "%s - - [%s] %s\n" % (
189
self.address_string(), self.log_date_time_string(), format % args)
193
self.address_string(), self.log_date_time_string(), format%args)
190
194
self.logger.info(message)
193
197
class TestPutMessage(TestCase):
195
199
def test_put_message(self):
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):
200
client = GrackleClient('localhost', 8436)
201
with ForkedFake.from_client(client):
202
client.put_message('arch1', 'asdf', StringIO('This is a message'))
209
203
with ExpectedException(Exception, 'wtf'):
210
client.put_message('no-archive', 'id1', StringIO('message'))
204
client.put_message('arch1', 'asdf',
205
StringIO('This is not a message'))
213
208
class TestGetMessages(TestCase):
334
316
def test_get_messages_unsupported_order(self):
335
317
client = GrackleClient('localhost', 8439)
338
make_message('foo', headers={'date': '2011-03-25'}),
339
make_message('foo', headers={'date': '2011-03-24'}),
341
with ForkedFakeService.from_client(client, archive):
342
with ExpectedException(UnsupportedOrder, ''):
318
with ForkedFake.from_client(client,
319
{'baz': [{'message_id': 'foo', 'date': '2011-03-25'},
320
{'message_id': 'bar', 'date': '2011-03-24'}]}):
321
with ExpectedException(UnsupportedOrder):
343
322
client.get_messages('baz', order='nonsense')
345
324
def test_get_messages_headers_no_headers(self):
346
325
client = GrackleClient('localhost', 8440)
347
archive = {'baz': [make_message('foo')]}
348
with ForkedFakeService.from_client(client, archive):
326
with ForkedFake.from_client(client,
328
{'message_id': 'foo'}
349
330
response = client.get_messages('baz', headers=[
350
331
'Subject', 'Date', 'X-Launchpad-Message-Rationale'])
351
332
first_message = response['messages'][0]
378
360
def test_get_messages_max_body_length(self):
379
361
client = GrackleClient('localhost', 8443)
380
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
381
with ForkedFakeService.from_client(client, archive):
362
with ForkedFake.from_client(client,
364
{'message_id': 'foo', 'body': u'abcdefghi'}
382
366
response = client.get_messages('baz', max_body_length=3)
383
367
first_message = response['messages'][0]
384
368
self.assertEqual('abc', first_message['body'])
386
370
def test_include_hidden(self):
387
371
client = GrackleClient('localhost', 8444)
390
make_message('foo', hidden=True),
391
make_message('bar', hidden=False),
393
with ForkedFakeService.from_client(client, archive):
372
with ForkedFake.from_client(client,
374
{'message_id': 'foo', 'hidden': True},
375
{'message_id': 'bar', 'hidden': False}
394
377
response = client.get_messages('baz', include_hidden=True)
395
378
self.assertMessageIDs(['bar', 'foo'], response['messages'])
396
379
response = client.get_messages('baz', include_hidden=False)
397
380
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'])