3
3
BaseHTTPRequestHandler,
8
7
from signal import SIGKILL
10
8
from StringIO import StringIO
12
9
from unittest import TestCase
13
from urlparse import urlparse
14
from urlparse import parse_qs
16
11
from testtools import ExpectedException
18
13
from grackle.client import (
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()
40
"""A memory-backed message store."""
42
def __init__(self, messages):
44
self.messages = messages
46
def get_messages(self, archive_id, query_string):
47
"""Return matching messages.
49
:param archive_id: The archive to retrieve from.
50
:param query_string: Contains 'parameters', which is a JSON-format
51
string describing parameters.
53
query = parse_qs(query_string)
54
parameters = simplejson.loads(query['parameters'][0])
55
order = parameters.get('order')
56
messages = self.messages[archive_id]
58
if order not in SUPPORTED_ORDERS:
59
raise UnsupportedOrder
60
elif order.startswith('thread_'):
61
threaded = threaded_messages(messages)
63
if order == 'thread_subject':
64
threaded.sort(key=lambda t: t[0]['subject'])
65
if order == 'thread_oldest':
66
threaded.sort(key=lambda t: min(m['date'] for m in t))
67
if order == 'thread_newest':
68
threaded.sort(key=lambda t: max(m['date'] for m in t))
69
for thread in threaded:
70
messages.extend(thread)
72
messages.sort(key=lambda m: m[order])
74
for message in messages:
75
if (not parameters['include_hidden']
76
and message.get('hidden', False)):
79
if ('message_ids' in parameters
80
and message['message_id'] not in parameters['message_ids']):
82
message = dict(message)
83
if 'headers' in parameters:
85
(k, v) for k, v in message['headers'].iteritems()
86
if k in parameters['headers'])
87
message['headers'] = headers
88
max_body = parameters.get('max_body_length')
89
if max_body is not None:
90
message['body'] = message['body'][:max_body]
91
new_messages.append(message)
92
messages = new_messages
93
limit = parameters.get('limit', 100)
94
memo = parameters.get('memo')
95
message_id_indices = dict(
96
(m['message_id'], idx) for idx, m in enumerate(messages))
100
start = message_id_indices[memo.encode('rot13')]
102
previous_memo = messages[start - 1]['message_id'].encode('rot13')
105
end = min(start + limit, len(messages))
106
if end < len(messages):
107
next_memo = messages[end]['message_id'].encode('rot13')
110
messages = messages[start:end]
113
'messages': messages,
114
'next_memo': next_memo,
115
'previous_memo': previous_memo
120
class ForkedFakeService:
121
"""A Grackle service fake, as a ContextManager."""
123
def __init__(self, port, messages=None, write_logs=False):
126
:param port: The tcp port to use.
127
:param messages: A dict of lists of dicts representing messages. The
128
outer dict represents the archive, the list represents the list of
129
messages for that archive.
130
:param write_logs: If true, log messages will be written to stdout.
20
def __init__(self, func_or_method, *args):
21
self.func_or_method = func_or_method
137
self.messages = messages
138
self.read_end, self.write_end = os.pipe()
139
self.write_logs = write_logs
142
def from_client(client, messages=None):
143
"""Instantiate a ForkedFakeService from the client.
145
:param port: The client to provide service for.
146
:param messages: A dict of lists of dicts representing messages. The
147
outer dict represents the archive, the list represents the list of
148
messages for that archive.
150
return ForkedFakeService(client.port, messages)
153
"""Tell the parent process that the server is ready for writes."""
154
os.write(self.write_end, 'asdf')
156
25
def __enter__(self):
159
Fork and start a server in the child. Return when the server is ready
165
os.read(self.read_end, 1)
30
self.func_or_method(*self.args)
168
def start_server(self):
169
"""Start the HTTP server."""
170
service = HTTPServer(('', self.port), FakeGrackleRequestHandler)
171
service.store = GrackleStore(self.messages)
172
for archive_id, messages in service.store.messages.iteritems():
173
for message in messages:
174
message.setdefault('headers', {})
178
stream=sys.stderr, level=logging.INFO)
179
service.serve_forever()
181
33
def __exit__(self, exc_type, exc_val, traceback):
182
34
os.kill(self.pid, SIGKILL)
185
SUPPORTED_ORDERS = set(
186
['date', 'author', 'subject', 'thread_newest', 'thread_oldest',
190
37
class FakeGrackleRequestHandler(BaseHTTPRequestHandler):
191
"""A request handler that forwards to server.store."""
193
def __init__(self, *args, **kwargs):
194
"""Constructor. Sets up logging."""
195
self.logger = logging.getLogger('http')
196
BaseHTTPRequestHandler.__init__(self, *args, **kwargs)
199
"""Create a message on POST."""
200
40
message = self.rfile.read(int(self.headers['content-length']))
201
41
if message == 'This is a message':
202
42
self.send_response(httplib.CREATED)
206
46
self.send_error(httplib.BAD_REQUEST)
209
"""Retrieve a list of messages on GET."""
210
scheme, netloc, path, params, query_string, fragments = (
212
parts = path.split('/')
213
if parts[1] == 'archive':
215
response = self.server.store.get_messages(
216
parts[2], query_string)
217
self.send_response(httplib.OK)
219
self.wfile.write(simplejson.dumps(response))
220
except UnsupportedOrder:
221
self.send_response(httplib.BAD_REQUEST)
222
self.wfile.write('Unsupported order')
225
def log_message(self, format, *args):
226
"""Override log_message to use standard Python logging."""
227
message = "%s - - [%s] %s\n" % (
228
self.address_string(), self.log_date_time_string(), format % args)
229
self.logger.info(message)
49
def run_service(port):
50
service = HTTPServer(('', port), FakeGrackleRequestHandler)
51
service.serve_forever()
232
55
class TestPutMessage(TestCase):
234
57
def test_put_message(self):
235
client = GrackleClient('localhost', 8436)
236
with ForkedFakeService.from_client(client):
58
client = GrackleClient('localhost', 8435)
59
with Forked(run_service, client.port):
237
60
client.put_message('arch1', 'asdf', StringIO('This is a message'))
238
61
with ExpectedException(Exception, 'wtf'):
239
62
client.put_message('arch1', 'asdf',
240
63
StringIO('This is not a message'))
243
class TestGetMessages(TestCase):
245
def assertIDOrder(self, ids, messages):
246
self.assertEqual(ids, [m['message_id'] for m in messages])
248
def assertMessageIDs(self, ids, messages):
250
sorted(ids), sorted(messages, key=lambda m: m['message_id']))
252
def test_get_messages(self):
253
client = GrackleClient('localhost', 8435)
254
with ForkedFakeService.from_client(client,
256
[{'message_id': 'foo'},
257
{'message_id': 'bar'}]}):
258
response = client.get_messages('baz')
259
self.assertEqual(['bar', 'foo'], sorted(m['message_id'] for m in
260
response['messages']))
261
self.assertIs(None, response['next_memo'])
262
self.assertIs(None, response['previous_memo'])
264
def test_get_messages_by_id(self):
265
client = GrackleClient('localhost', 8437)
266
with ForkedFakeService.from_client(client,
268
[{'message_id': 'foo'},
269
{'message_id': 'bar'}]}):
270
response = client.get_messages('baz', message_ids=['foo'])
271
message, = response['messages']
272
self.assertEqual('foo', message['message_id'])
274
def test_get_messages_batching(self):
275
client = GrackleClient('localhost', 8438)
276
with ForkedFakeService.from_client(client,
278
[{'message_id': 'foo'},
279
{'message_id': 'bar'}]}):
280
response = client.get_messages('baz', limit=1)
281
self.assertEqual(1, len(response['messages']))
282
messages = response['messages']
283
response = client.get_messages(
284
'baz', limit=1, memo=response['next_memo'])
285
self.assertEqual(1, len(response['messages']))
286
messages.extend(response['messages'])
287
self.assertMessageIDs(['foo', 'bar'], messages)
289
def get_messages_member_order_test(self, key):
290
client = GrackleClient('localhost', 8439)
291
with ForkedFakeService.from_client(client,
292
{'baz': [{'message_id': 'foo', key: '2011-03-25'},
293
{'message_id': 'bar', key: '2011-03-24'}]}):
294
response = client.get_messages('baz')
295
self.assertIDOrder(['foo', 'bar'], response['messages'])
296
response = client.get_messages('baz', order=key)
297
self.assertIDOrder(['bar', 'foo'], response['messages'])
299
def test_get_messages_date_order(self):
300
self.get_messages_member_order_test('date')
302
def test_get_messages_author_order(self):
303
self.get_messages_member_order_test('author')
305
def test_get_messages_subject_order(self):
306
self.get_messages_member_order_test('subject')
308
def test_get_messages_thread_subject_order(self):
309
client = GrackleClient('localhost', 8439)
310
with ForkedFakeService.from_client(client, {'baz': [
311
{'message_id': 'bar', 'subject': 'y'},
312
{'message_id': 'qux', 'subject': 'z'},
313
{'message_id': 'foo', 'subject': 'x', 'in_reply_to': 'qux'},
315
response = client.get_messages('baz')
316
self.assertIDOrder(['bar', 'qux', 'foo'], response['messages'])
317
response = client.get_messages('baz', order='subject')
318
self.assertIDOrder(['foo', 'bar', 'qux'], response['messages'])
319
response = client.get_messages('baz', order='thread_subject')
320
self.assertIDOrder(['bar', 'qux', 'foo'], response['messages'])
322
def test_get_messages_thread_oldest_order(self):
323
client = GrackleClient('localhost', 8439)
324
with ForkedFakeService.from_client(client, {'baz': [
325
{'message_id': 'bar', 'date': 'x'},
326
{'message_id': 'qux', 'date': 'z'},
327
{'message_id': 'foo', 'date': 'y', 'in_reply_to': 'qux'},
329
response = client.get_messages('baz')
330
self.assertIDOrder(['bar', 'qux', 'foo'], response['messages'])
331
response = client.get_messages('baz', order='date')
332
self.assertIDOrder(['bar', 'foo', 'qux'], response['messages'])
333
response = client.get_messages('baz', order='thread_oldest')
334
self.assertIDOrder(['bar', 'qux', 'foo'], response['messages'])
336
def test_get_messages_thread_newest_order(self):
337
client = GrackleClient('localhost', 8439)
338
with ForkedFakeService.from_client(client, {'baz': [
339
{'message_id': 'bar', 'date': 'x'},
340
{'message_id': 'qux', 'date': 'w'},
341
{'message_id': 'foo', 'date': 'y', 'in_reply_to': 'bar'},
342
{'message_id': 'baz', 'date': 'z', 'in_reply_to': 'qux'},
344
response = client.get_messages('baz', order='date')
346
['qux', 'bar', 'foo', 'baz'], response['messages'])
347
response = client.get_messages('baz', order='thread_newest')
349
['bar', 'foo', 'qux', 'baz'], response['messages'])
351
def test_get_messages_unsupported_order(self):
352
client = GrackleClient('localhost', 8439)
353
with ForkedFakeService.from_client(client,
354
{'baz': [{'message_id': 'foo', 'date': '2011-03-25'},
355
{'message_id': 'bar', 'date': '2011-03-24'}]}):
356
with ExpectedException(UnsupportedOrder, ''):
357
client.get_messages('baz', order='nonsense')
359
def test_get_messages_headers_no_headers(self):
360
client = GrackleClient('localhost', 8440)
361
with ForkedFakeService.from_client(client,
363
{'message_id': 'foo'}
365
response = client.get_messages('baz', headers=[
366
'Subject', 'Date', 'X-Launchpad-Message-Rationale'])
367
first_message = response['messages'][0]
368
self.assertEqual('foo', first_message['message_id'])
369
self.assertEqual({}, first_message['headers'])
371
def test_get_messages_headers_exclude_headers(self):
372
client = GrackleClient('localhost', 8441)
373
with ForkedFakeService.from_client(client,
375
{'message_id': 'foo', 'headers': {'From': 'me'}}
377
response = client.get_messages('baz', headers=[
378
'Subject', 'Date', 'X-Launchpad-Message-Rationale'])
379
first_message = response['messages'][0]
380
self.assertEqual('foo', first_message['message_id'])
381
self.assertEqual({}, first_message['headers'])
383
def test_get_messages_headers_include_headers(self):
384
client = GrackleClient('localhost', 8442)
385
with ForkedFakeService.from_client(client,
387
{'message_id': 'foo', 'headers': {'From': 'me', 'To': 'you'}}
389
response = client.get_messages('baz', headers=[
391
first_message = response['messages'][0]
392
self.assertEqual('foo', first_message['message_id'])
393
self.assertEqual({'From': 'me', 'To': 'you'}, first_message['headers'])
395
def test_get_messages_max_body_length(self):
396
client = GrackleClient('localhost', 8443)
397
with ForkedFakeService.from_client(client,
399
{'message_id': 'foo', 'body': u'abcdefghi'}
401
response = client.get_messages('baz', max_body_length=3)
402
first_message = response['messages'][0]
403
self.assertEqual('abc', first_message['body'])
405
def test_include_hidden(self):
406
client = GrackleClient('localhost', 8444)
407
with ForkedFakeService.from_client(client,
409
{'message_id': 'foo', 'hidden': True},
410
{'message_id': 'bar', 'hidden': False}
412
response = client.get_messages('baz', include_hidden=True)
413
self.assertMessageIDs(['bar', 'foo'], response['messages'])
414
response = client.get_messages('baz', include_hidden=False)
415
self.assertMessageIDs(['bar'], response['messages'])