3
3
BaseHTTPRequestHandler,
8
6
from signal import SIGKILL
10
7
from StringIO import StringIO
12
8
from unittest import TestCase
13
from urlparse import urlparse
14
from urlparse import parse_qs
16
10
from testtools import ExpectedException
18
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]
57
if order is not None :
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:
76
not parameters['include_hidden']
77
and message.get('hidden', False)):
80
if ('message_ids' in parameters and
81
message['message_id'] not in parameters['message_ids']):
83
message = dict(message)
84
if 'headers' in parameters:
86
(k, v) for k, v in message['headers'].iteritems()
87
if k in parameters['headers'])
88
message['headers'] = headers
89
max_body = parameters.get('max_body_length')
90
if max_body is not None:
91
message['body'] = message['body'][:max_body]
92
new_messages.append(message)
93
messages = new_messages
94
limit = parameters.get('limit', 100)
95
memo = parameters.get('memo')
96
message_id_indices = dict(
97
(m['message_id'], idx) for idx, m in enumerate(messages))
101
start = message_id_indices[memo.encode('rot13')]
103
previous_memo = messages[start - 1]['message_id'].encode('rot13')
106
end = min(start + limit, len(messages))
107
if end < len(messages):
108
next_memo = messages[end]['message_id'].encode('rot13')
111
messages = messages[start:end]
114
'messages': messages,
115
'next_memo': next_memo,
116
'previous_memo': previous_memo
123
"""A Grackle service fake, as a ContextManager."""
125
def __init__(self, port, messages=None, write_logs=False):
127
:param port: The tcp port to use
128
:param messages: A dict of lists of dicts representing messages. The
129
outer dict represents the archive, the list represents the list of
130
messages for that archive.
131
:param write_logs: If true, log messages will be written to stdout.
12
from grackle import client
17
def __init__(self, func_or_method):
18
self.func_or_method = func_or_method
138
self.messages = messages
139
self.read_end, self.write_end = os.pipe()
140
self.write_logs = write_logs
143
def from_client(client, messages=None):
144
"""Instantiate a ForkedFake from the client.
146
:param port: The client to provide service for.
147
:param messages: A dict of lists of dicts representing messages. The
148
outer dict represents the archive, the list represents the list of
149
messages for that archive.
151
return ForkedFake(client.port, messages)
154
"""Tell the parent process that the server is ready for writes."""
155
os.write(self.write_end, 'asdf')
157
21
def __enter__(self):
160
Fork and start a server in the child. Return when the server is ready
166
os.read(self.read_end, 1)
169
def start_server(self):
170
"""Start the HTTP server."""
171
service = HTTPServer(('', self.port), FakeGrackleRequestHandler)
172
service.store = GrackleStore(self.messages)
173
for archive_id, messages in service.store.messages.iteritems():
174
for message in messages:
175
message.setdefault('headers', {})
179
stream=sys.stderr, level=logging.INFO)
180
service.serve_forever()
182
29
def __exit__(self, exc_type, exc_val, traceback):
183
30
os.kill(self.pid, SIGKILL)
186
SUPPORTED_ORDERS = set(
187
['date', 'author', 'subject', 'thread_newest', 'thread_oldest',
191
33
class FakeGrackleRequestHandler(BaseHTTPRequestHandler):
192
"""A request handler that forwards to server.store."""
194
def __init__(self, *args, **kwargs):
195
"""Constructor. Sets up logging."""
196
self.logger = logging.getLogger('http')
197
BaseHTTPRequestHandler.__init__(self, *args, **kwargs)
199
35
def do_POST(self):
200
"""Create a message on POST."""
201
36
message = self.rfile.read(int(self.headers['content-length']))
202
37
if message == 'This is a message':
203
self.send_response(httplib.CREATED)
38
self.send_response(200)
204
39
self.end_headers()
205
40
self.wfile.close()
207
self.send_error(httplib.BAD_REQUEST)
210
"""Retrieve a list of messages on GET."""
211
scheme, netloc, path, params, query_string, fragments = (
213
parts = path.split('/')
214
if parts[1] == 'archive':
216
response = self.server.store.get_messages(
217
parts[2], query_string)
218
self.send_response(httplib.OK)
220
self.wfile.write(simplejson.dumps(response))
221
except UnsupportedOrder:
222
self.send_response(httplib.BAD_REQUEST)
223
self.wfile.write('Unsupported order')
226
def log_message(self, format, *args):
227
"""Override log_message to use standard Python logging."""
228
message = "%s - - [%s] %s\n" % (
229
self.address_string(), self.log_date_time_string(), format%args)
230
self.logger.info(message)
46
service = HTTPServer(('', 8435), FakeGrackleRequestHandler)
47
service.serve_forever()
233
51
class TestPutMessage(TestCase):
235
53
def test_put_message(self):
236
client = GrackleClient('localhost', 8436)
237
with ForkedFake.from_client(client):
238
client.put_message('arch1', 'asdf', StringIO('This is a message'))
54
with Forked(run_service):
55
client.put_message('arch1', StringIO('This is a message'))
239
56
with ExpectedException(Exception, 'wtf'):
240
client.put_message('arch1', 'asdf',
241
StringIO('This is not a message'))
244
class TestGetMessages(TestCase):
246
def assertIDOrder(self, ids, messages):
247
self.assertEqual(ids, [m['message_id'] for m in messages])
249
def assertMessageIDs(self, ids, messages):
251
sorted(ids), sorted(messages, key=lambda m:m['message_id']))
253
def test_get_messages(self):
254
client = GrackleClient('localhost', 8435)
255
with ForkedFake.from_client(client,
257
[{'message_id': 'foo'},
258
{'message_id': 'bar'}]}):
259
response = client.get_messages('baz')
260
self.assertEqual(['bar', 'foo'], sorted(m['message_id'] for m in
261
response['messages']))
262
self.assertIs(None, response['next_memo'])
263
self.assertIs(None, response['previous_memo'])
265
def test_get_messages_by_id(self):
266
client = GrackleClient('localhost', 8437)
267
with ForkedFake.from_client(client,
269
[{'message_id': 'foo'},
270
{'message_id': 'bar'}]}):
271
response = client.get_messages('baz', message_ids=['foo'])
272
message, = response['messages']
273
self.assertEqual('foo', message['message_id'])
275
def test_get_messages_batching(self):
276
client = GrackleClient('localhost', 8438)
277
with ForkedFake.from_client(client,
279
[{'message_id': 'foo'},
280
{'message_id': 'bar'}]}):
281
response = client.get_messages('baz', limit=1)
282
self.assertEqual(1, len(response['messages']))
283
messages = response['messages']
284
response = client.get_messages(
285
'baz', limit=1, memo=response['next_memo'])
286
self.assertEqual(1, len(response['messages']))
287
messages.extend(response['messages'])
288
self.assertMessageIDs(['foo', 'bar'], messages)
290
def get_messages_member_order_test(self, key):
291
client = GrackleClient('localhost', 8439)
292
with ForkedFake.from_client(client,
293
{'baz': [{'message_id': 'foo', key: '2011-03-25'},
294
{'message_id': 'bar', key: '2011-03-24'}]}):
295
response = client.get_messages('baz')
296
self.assertIDOrder(['foo', 'bar'], response['messages'])
297
response = client.get_messages('baz', order=key)
298
self.assertIDOrder(['bar', 'foo'], response['messages'])
300
def test_get_messages_date_order(self):
301
self.get_messages_member_order_test('date')
303
def test_get_messages_author_order(self):
304
self.get_messages_member_order_test('author')
306
def test_get_messages_subject_order(self):
307
self.get_messages_member_order_test('subject')
309
def test_get_messages_thread_subject_order(self):
310
client = GrackleClient('localhost', 8439)
311
with ForkedFake.from_client(client, {'baz': [
312
{'message_id': 'bar', 'subject': 'y'},
313
{'message_id': 'qux', 'subject': 'z'},
314
{'message_id': 'foo', 'subject': 'x', 'in_reply_to': 'qux'},
316
response = client.get_messages('baz')
317
self.assertIDOrder(['bar', 'qux', 'foo'], response['messages'])
318
response = client.get_messages('baz', order='subject')
319
self.assertIDOrder(['foo', 'bar', 'qux'], response['messages'])
320
response = client.get_messages('baz', order='thread_subject')
321
self.assertIDOrder(['bar', 'qux', 'foo'], response['messages'])
323
def test_get_messages_thread_oldest_order(self):
324
client = GrackleClient('localhost', 8439)
325
with ForkedFake.from_client(client, {'baz': [
326
{'message_id': 'bar', 'date': 'x'},
327
{'message_id': 'qux', 'date': 'z'},
328
{'message_id': 'foo', 'date': 'y', 'in_reply_to': 'qux'},
330
response = client.get_messages('baz')
331
self.assertIDOrder(['bar', 'qux', 'foo'], response['messages'])
332
response = client.get_messages('baz', order='date')
333
self.assertIDOrder(['bar', 'foo', 'qux'], response['messages'])
334
response = client.get_messages('baz', order='thread_oldest')
335
self.assertIDOrder(['bar', 'qux', 'foo'], response['messages'])
337
def test_get_messages_thread_newest_order(self):
338
client = GrackleClient('localhost', 8439)
339
with ForkedFake.from_client(client, {'baz': [
340
{'message_id': 'bar', 'date': 'x'},
341
{'message_id': 'qux', 'date': 'w'},
342
{'message_id': 'foo', 'date': 'y', 'in_reply_to': 'bar'},
343
{'message_id': 'baz', 'date': 'z', 'in_reply_to': 'qux'},
345
response = client.get_messages('baz', order='date')
347
['qux', 'bar', 'foo', 'baz'], response['messages'])
348
response = client.get_messages('baz', order='thread_newest')
350
['bar', 'foo', 'qux', 'baz'], response['messages'])
352
def test_get_messages_unsupported_order(self):
353
client = GrackleClient('localhost', 8439)
354
with ForkedFake.from_client(client,
355
{'baz': [{'message_id': 'foo', 'date': '2011-03-25'},
356
{'message_id': 'bar', 'date': '2011-03-24'}]}):
357
with ExpectedException(UnsupportedOrder, ''):
358
client.get_messages('baz', order='nonsense')
360
def test_get_messages_headers_no_headers(self):
361
client = GrackleClient('localhost', 8440)
362
with ForkedFake.from_client(client,
364
{'message_id': 'foo'}
366
response = client.get_messages('baz', headers=[
367
'Subject', 'Date', 'X-Launchpad-Message-Rationale'])
368
first_message = response['messages'][0]
369
self.assertEqual('foo', first_message['message_id'])
370
self.assertEqual({}, first_message['headers'])
372
def test_get_messages_headers_exclude_headers(self):
373
client = GrackleClient('localhost', 8441)
374
with ForkedFake.from_client(client,
376
{'message_id': 'foo', 'headers': {'From': 'me'}}
378
response = client.get_messages('baz', headers=[
379
'Subject', 'Date', 'X-Launchpad-Message-Rationale'])
380
first_message = response['messages'][0]
381
self.assertEqual('foo', first_message['message_id'])
382
self.assertEqual({}, first_message['headers'])
384
def test_get_messages_headers_include_headers(self):
385
client = GrackleClient('localhost', 8442)
386
with ForkedFake.from_client(client,
388
{'message_id': 'foo', 'headers': {'From': 'me', 'To': 'you'}}
390
response = client.get_messages('baz', headers=[
392
first_message = response['messages'][0]
393
self.assertEqual('foo', first_message['message_id'])
394
self.assertEqual({'From': 'me', 'To': 'you'}, first_message['headers'])
396
def test_get_messages_max_body_length(self):
397
client = GrackleClient('localhost', 8443)
398
with ForkedFake.from_client(client,
400
{'message_id': 'foo', 'body': u'abcdefghi'}
402
response = client.get_messages('baz', max_body_length=3)
403
first_message = response['messages'][0]
404
self.assertEqual('abc', first_message['body'])
406
def test_include_hidden(self):
407
client = GrackleClient('localhost', 8444)
408
with ForkedFake.from_client(client,
410
{'message_id': 'foo', 'hidden': True},
411
{'message_id': 'bar', 'hidden': False}
413
response = client.get_messages('baz', include_hidden=True)
414
self.assertMessageIDs(['bar', 'foo'], response['messages'])
415
response = client.get_messages('baz', include_hidden=False)
416
self.assertMessageIDs(['bar'], response['messages'])
57
client.put_message('arch1', StringIO('This is not a message'))