1
from BaseHTTPServer import (
3
BaseHTTPRequestHandler,
8
from signal import SIGKILL
10
from StringIO import StringIO
12
1
from unittest import TestCase
13
from urlparse import urlparse
14
from urlparse import parse_qs
16
from testtools import ExpectedException
18
from grackle.client import (
20
UnsupportedDisplayType,
25
def threaded_messages(messages):
29
for message in messages:
30
if message.get('in_reply_to') is None:
31
threads[message['message_id']] = [message]
34
pending.append(message)
35
for message in pending:
36
threads[message['in_reply_to']].append(message)
37
return threads.values()
41
"""A memory-backed message store."""
43
def __init__(self, messages):
45
self.messages = messages
47
def get_messages(self, archive_id, query_string):
48
"""Return matching messages.
50
:param archive_id: The archive to retrieve from.
51
:param query_string: Contains 'parameters', which is a JSON-format
52
string describing parameters.
54
query = parse_qs(query_string)
55
parameters = simplejson.loads(query['parameters'][0])
56
order = parameters.get('order')
57
messages = self.messages[archive_id]
59
if order not in SUPPORTED_ORDERS:
60
raise UnsupportedOrder
61
elif order.startswith('thread_'):
62
threaded = threaded_messages(messages)
64
if order == 'thread_subject':
65
threaded.sort(key=lambda t: t[0]['subject'])
66
if order == 'thread_oldest':
67
threaded.sort(key=lambda t: min(m['date'] for m in t))
68
if order == 'thread_newest':
69
threaded.sort(key=lambda t: max(m['date'] for m in t))
70
for thread in threaded:
71
messages.extend(thread)
73
messages.sort(key=lambda m: m[order])
74
display_type = parameters.get('display_type', 'all')
75
if display_type not in SUPPORTED_DISPLAY_TYPES:
76
raise UnsupportedDisplayType
78
for message in messages:
79
if (not parameters['include_hidden']
80
and message.get('hidden', False)):
83
if ('message_ids' in parameters
84
and message['message_id'] not in parameters['message_ids']):
86
message = dict(message)
87
if 'headers' in parameters:
89
(k, v) for k, v in message['headers'].iteritems()
90
if k in parameters['headers'])
91
message['headers'] = headers
92
max_body = parameters.get('max_body_length')
93
if max_body is not None:
94
message['body'] = message['body'][:max_body]
95
new_messages.append(message)
96
messages = new_messages
97
limit = parameters.get('limit', 100)
98
memo = parameters.get('memo')
99
message_id_indices = dict(
100
(m['message_id'], idx) for idx, m in enumerate(messages))
104
start = message_id_indices[memo.encode('rot13')]
106
previous_memo = messages[start - 1]['message_id'].encode('rot13')
109
end = min(start + limit, len(messages))
110
if end < len(messages):
111
next_memo = messages[end]['message_id'].encode('rot13')
114
messages = messages[start:end]
117
'messages': messages,
118
'next_memo': next_memo,
119
'previous_memo': previous_memo
124
class ForkedFakeService:
125
"""A Grackle service fake, as a ContextManager."""
127
def __init__(self, port, messages=None, write_logs=False):
130
:param port: The tcp port to use.
131
:param messages: A dict of lists of dicts representing messages. The
132
outer dict represents the archive, the list represents the list of
133
messages for that archive.
134
:param write_logs: If true, log messages will be written to stdout.
141
self.messages = messages
142
self.read_end, self.write_end = os.pipe()
143
self.write_logs = write_logs
146
def from_client(client, messages=None):
147
"""Instantiate a ForkedFakeService from the client.
149
:param port: The client to provide service for.
150
:param messages: A dict of lists of dicts representing messages. The
151
outer dict represents the archive, the list represents the list of
152
messages for that archive.
154
return ForkedFakeService(client.port, messages)
157
"""Tell the parent process that the server is ready for writes."""
158
os.write(self.write_end, 'asdf')
163
Fork and start a server in the child. Return when the server is ready
169
os.read(self.read_end, 1)
172
def start_server(self):
173
"""Start the HTTP server."""
174
service = HTTPServer(('', self.port), FakeGrackleRequestHandler)
175
service.store = GrackleStore(self.messages)
176
for archive_id, messages in service.store.messages.iteritems():
177
for message in messages:
178
message.setdefault('headers', {})
182
stream=sys.stderr, level=logging.INFO)
183
service.serve_forever()
185
def __exit__(self, exc_type, exc_val, traceback):
186
os.kill(self.pid, SIGKILL)
189
SUPPORTED_DISPLAY_TYPES = set(['all', 'text-only', 'headers-only'])
192
SUPPORTED_ORDERS = set(
193
['date', 'author', 'subject', 'thread_newest', 'thread_oldest',
197
class FakeGrackleRequestHandler(BaseHTTPRequestHandler):
198
"""A request handler that forwards to server.store."""
200
def __init__(self, *args, **kwargs):
201
"""Constructor. Sets up logging."""
202
self.logger = logging.getLogger('http')
203
BaseHTTPRequestHandler.__init__(self, *args, **kwargs)
206
"""Create a message on POST."""
207
message = self.rfile.read(int(self.headers['content-length']))
208
if message == 'This is a message':
209
self.send_response(httplib.CREATED)
213
self.send_error(httplib.BAD_REQUEST)
216
"""Retrieve a list of messages on GET."""
217
scheme, netloc, path, params, query_string, fragments = (
219
parts = path.split('/')
220
if parts[1] == 'archive':
222
response = self.server.store.get_messages(
223
parts[2], query_string)
224
self.send_response(httplib.OK)
226
self.wfile.write(simplejson.dumps(response))
227
except UnsupportedOrder:
228
self.send_response(httplib.BAD_REQUEST)
229
self.wfile.write('Unsupported order')
232
def log_message(self, format, *args):
233
"""Override log_message to use standard Python logging."""
234
message = "%s - - [%s] %s\n" % (
235
self.address_string(), self.log_date_time_string(), format % args)
236
self.logger.info(message)
239
3
class TestPutMessage(TestCase):
241
5
def test_put_message(self):
242
client = GrackleClient('localhost', 8436)
243
with ForkedFakeService.from_client(client):
244
client.put_message('arch1', 'asdf', StringIO('This is a message'))
245
with ExpectedException(Exception, 'wtf'):
246
client.put_message('arch1', 'asdf',
247
StringIO('This is not a message'))
250
class TestGetMessages(TestCase):
252
def assertIDOrder(self, ids, messages):
253
self.assertEqual(ids, [m['message_id'] for m in messages])
255
def assertMessageIDs(self, ids, messages):
257
sorted(ids), sorted(messages, key=lambda m: m['message_id']))
259
def test_get_messages(self):
260
client = GrackleClient('localhost', 8435)
261
with ForkedFakeService.from_client(client,
263
[{'message_id': 'foo'},
264
{'message_id': 'bar'}]}):
265
response = client.get_messages('baz')
266
self.assertEqual(['bar', 'foo'], sorted(m['message_id'] for m in
267
response['messages']))
268
self.assertIs(None, response['next_memo'])
269
self.assertIs(None, response['previous_memo'])
271
def test_get_messages_by_id(self):
272
client = GrackleClient('localhost', 8437)
273
with ForkedFakeService.from_client(client,
275
[{'message_id': 'foo'},
276
{'message_id': 'bar'}]}):
277
response = client.get_messages('baz', message_ids=['foo'])
278
message, = response['messages']
279
self.assertEqual('foo', message['message_id'])
281
def test_get_messages_batching(self):
282
client = GrackleClient('localhost', 8438)
283
with ForkedFakeService.from_client(client,
285
[{'message_id': 'foo'},
286
{'message_id': 'bar'}]}):
287
response = client.get_messages('baz', limit=1)
288
self.assertEqual(1, len(response['messages']))
289
messages = response['messages']
290
response = client.get_messages(
291
'baz', limit=1, memo=response['next_memo'])
292
self.assertEqual(1, len(response['messages']))
293
messages.extend(response['messages'])
294
self.assertMessageIDs(['foo', 'bar'], messages)
296
def get_messages_member_order_test(self, key):
297
client = GrackleClient('localhost', 8439)
298
with ForkedFakeService.from_client(client,
299
{'baz': [{'message_id': 'foo', key: '2011-03-25'},
300
{'message_id': 'bar', key: '2011-03-24'}]}):
301
response = client.get_messages('baz')
302
self.assertIDOrder(['foo', 'bar'], response['messages'])
303
response = client.get_messages('baz', order=key)
304
self.assertIDOrder(['bar', 'foo'], response['messages'])
306
def test_get_messages_date_order(self):
307
self.get_messages_member_order_test('date')
309
def test_get_messages_author_order(self):
310
self.get_messages_member_order_test('author')
312
def test_get_messages_subject_order(self):
313
self.get_messages_member_order_test('subject')
315
def test_get_messages_thread_subject_order(self):
316
client = GrackleClient('localhost', 8439)
317
with ForkedFakeService.from_client(client, {'baz': [
318
{'message_id': 'bar', 'subject': 'y'},
319
{'message_id': 'qux', 'subject': 'z'},
320
{'message_id': 'foo', 'subject': 'x', 'in_reply_to': 'qux'},
322
response = client.get_messages('baz')
323
self.assertIDOrder(['bar', 'qux', 'foo'], response['messages'])
324
response = client.get_messages('baz', order='subject')
325
self.assertIDOrder(['foo', 'bar', 'qux'], response['messages'])
326
response = client.get_messages('baz', order='thread_subject')
327
self.assertIDOrder(['bar', 'qux', 'foo'], response['messages'])
329
def test_get_messages_thread_oldest_order(self):
330
client = GrackleClient('localhost', 8439)
331
with ForkedFakeService.from_client(client, {'baz': [
332
{'message_id': 'bar', 'date': 'x'},
333
{'message_id': 'qux', 'date': 'z'},
334
{'message_id': 'foo', 'date': 'y', 'in_reply_to': 'qux'},
336
response = client.get_messages('baz')
337
self.assertIDOrder(['bar', 'qux', 'foo'], response['messages'])
338
response = client.get_messages('baz', order='date')
339
self.assertIDOrder(['bar', 'foo', 'qux'], response['messages'])
340
response = client.get_messages('baz', order='thread_oldest')
341
self.assertIDOrder(['bar', 'qux', 'foo'], response['messages'])
343
def test_get_messages_thread_newest_order(self):
344
client = GrackleClient('localhost', 8439)
345
with ForkedFakeService.from_client(client, {'baz': [
346
{'message_id': 'bar', 'date': 'x'},
347
{'message_id': 'qux', 'date': 'w'},
348
{'message_id': 'foo', 'date': 'y', 'in_reply_to': 'bar'},
349
{'message_id': 'baz', 'date': 'z', 'in_reply_to': 'qux'},
351
response = client.get_messages('baz', order='date')
353
['qux', 'bar', 'foo', 'baz'], response['messages'])
354
response = client.get_messages('baz', order='thread_newest')
356
['bar', 'foo', 'qux', 'baz'], response['messages'])
358
def test_get_messages_unsupported_order(self):
359
client = GrackleClient('localhost', 8439)
360
with ForkedFakeService.from_client(client,
361
{'baz': [{'message_id': 'foo', 'date': '2011-03-25'},
362
{'message_id': 'bar', 'date': '2011-03-24'}]}):
363
with ExpectedException(UnsupportedOrder, ''):
364
client.get_messages('baz', order='nonsense')
366
def test_get_messages_headers_no_headers(self):
367
client = GrackleClient('localhost', 8440)
368
with ForkedFakeService.from_client(client,
370
{'message_id': 'foo'}
372
response = client.get_messages('baz', headers=[
373
'Subject', 'Date', 'X-Launchpad-Message-Rationale'])
374
first_message = response['messages'][0]
375
self.assertEqual('foo', first_message['message_id'])
376
self.assertEqual({}, first_message['headers'])
378
def test_get_messages_headers_exclude_headers(self):
379
client = GrackleClient('localhost', 8441)
380
with ForkedFakeService.from_client(client,
382
{'message_id': 'foo', 'headers': {'From': 'me'}}
384
response = client.get_messages('baz', headers=[
385
'Subject', 'Date', 'X-Launchpad-Message-Rationale'])
386
first_message = response['messages'][0]
387
self.assertEqual('foo', first_message['message_id'])
388
self.assertEqual({}, first_message['headers'])
390
def test_get_messages_headers_include_headers(self):
391
client = GrackleClient('localhost', 8442)
392
with ForkedFakeService.from_client(client,
394
{'message_id': 'foo', 'headers': {'From': 'me', 'To': 'you'}}
396
response = client.get_messages('baz', headers=[
398
first_message = response['messages'][0]
399
self.assertEqual('foo', first_message['message_id'])
400
self.assertEqual({'From': 'me', 'To': 'you'}, first_message['headers'])
402
def test_get_messages_max_body_length(self):
403
client = GrackleClient('localhost', 8443)
404
with ForkedFakeService.from_client(client,
406
{'message_id': 'foo', 'body': u'abcdefghi'}
408
response = client.get_messages('baz', max_body_length=3)
409
first_message = response['messages'][0]
410
self.assertEqual('abc', first_message['body'])
412
def test_include_hidden(self):
413
client = GrackleClient('localhost', 8444)
414
with ForkedFakeService.from_client(client,
416
{'message_id': 'foo', 'hidden': True},
417
{'message_id': 'bar', 'hidden': False}
419
response = client.get_messages('baz', include_hidden=True)
420
self.assertMessageIDs(['bar', 'foo'], response['messages'])
421
response = client.get_messages('baz', include_hidden=False)
422
self.assertMessageIDs(['bar'], response['messages'])
424
def test_display_type_unknown_value(self):
425
client = GrackleClient('localhost', 8445)
426
with ForkedFakeService.from_client(client,
428
{'message_id': 'foo', 'body': u'abcdefghi'}
430
with ExpectedException(UnsupportedDisplayType, ''):
431
client.get_messages('baz', display_type='unknown')