3
3
BaseHTTPRequestHandler,
5
from email.message import Message
6
from email.mime.multipart import MIMEMultipart
7
from email.mime.text import MIMEText
11
7
from signal import SIGKILL
13
8
from StringIO import StringIO
15
9
from unittest import TestCase
16
from urlparse import urlparse
18
11
from testtools import ExpectedException
20
from grackle.client import (
23
UnsupportedDisplayType,
26
from grackle.store import (
32
def make_message(message_id, body='body', headers=None, hidden=False):
35
headers['Message-Id'] = message_id
37
'message_id': message_id,
39
'thread_id': message_id,
40
'date': headers.get('date', '2005-01-01'),
41
'subject': headers.get('subject', 'subject'),
42
'author': headers.get('author', 'author'),
45
'replies': headers.get('in-reply-to', None),
51
def make_mime_message(message_id, body='body', headers=None, hidden=False,
52
attachment_type=None):
53
message = MIMEMultipart()
54
message.attach(MIMEText(body))
57
for key, value in headers.items():
59
if attachment_type is not None:
60
attachment = Message()
61
attachment.set_payload('attactment data.')
62
attachment['Content-Type'] = attachment_type
63
attachment['Content-Disposition'] = 'attachment; filename="file.ext"'
64
message.attach(attachment)
65
return make_json_message(message_id, message.as_string())
68
class ForkedFakeService:
69
"""A Grackle service fake, as a ContextManager."""
71
def __init__(self, port, message_archives=None, write_logs=False):
74
:param port: The tcp port to use.
75
:param message_archives: A dict of lists of dicts representing
76
archives of messages. The outer dict represents the archive,
77
the list represents the list of messages for that archive.
78
:param write_logs: If true, log messages will be written to stdout.
13
from grackle import client
18
def __init__(self, func_or_method):
19
self.func_or_method = func_or_method
82
if message_archives is None:
83
self.message_archives = {}
85
self.message_archives = message_archives
86
self.read_end, self.write_end = os.pipe()
87
self.write_logs = write_logs
90
def from_client(client, message_archives=None):
91
"""Instantiate a ForkedFakeService from the client.
93
:param port: The client to provide service for.
94
:param message_archives: A dict of lists of dicts representing
95
archives of messages. The outer dict represents the archive,
96
the list represents the list of messages for that archive.
98
return ForkedFakeService(client.port, message_archives)
101
"""Tell the parent process that the server is ready for writes."""
102
os.write(self.write_end, 'asdf')
104
22
def __enter__(self):
107
Fork and start a server in the child. Return when the server is ready
113
os.read(self.read_end, 1)
116
def start_server(self):
117
"""Start the HTTP server."""
118
service = HTTPServer(('', self.port), FakeGrackleRequestHandler)
119
service.store = MemoryStore(self.message_archives)
120
for archive_id, messages in service.store.message_archives.iteritems():
121
for message in messages:
122
message.setdefault('headers', {})
126
stream=sys.stderr, level=logging.INFO)
127
service.serve_forever()
129
30
def __exit__(self, exc_type, exc_val, traceback):
130
31
os.kill(self.pid, SIGKILL)
133
34
class FakeGrackleRequestHandler(BaseHTTPRequestHandler):
134
"""A request handler that forwards to server.store."""
136
def __init__(self, *args, **kwargs):
137
"""Constructor. Sets up logging."""
138
self.logger = logging.getLogger('http')
139
BaseHTTPRequestHandler.__init__(self, *args, **kwargs)
141
36
def do_POST(self):
142
"""Create a message on POST."""
143
37
message = self.rfile.read(int(self.headers['content-length']))
144
scheme, netloc, path, params, query_string, fragments = (
146
parts = path.split('/')
147
if parts[1] == 'archive' and len(parts) == 4:
149
# This expected path is /archive/archive_id/message_id.
150
self.server.store.put_message(parts[2], parts[3], message)
151
self.send_response(httplib.CREATED)
155
self.send_error(httplib.BAD_REQUEST)
158
"""Retrieve a list of messages on GET."""
159
scheme, netloc, path, params, query_string, fragments = (
161
parts = path.split('/')
162
if parts[1] == 'archive':
164
response = self.server.store.get_messages(
165
parts[2], query_string)
166
self.send_response(httplib.OK)
168
self.wfile.write(simplejson.dumps(response))
169
except Exception, error:
171
httplib.BAD_REQUEST, error.__doc__)
174
def log_message(self, format, *args):
175
"""Override log_message to use standard Python logging."""
176
message = "%s - - [%s] %s\n" % (
177
self.address_string(), self.log_date_time_string(), format % args)
178
self.logger.info(message)
38
if message == 'This is a message':
39
self.send_response(httplib.CREATED)
43
self.send_error(httplib.BAD_REQUEST)
47
service = HTTPServer(('', 8435), FakeGrackleRequestHandler)
48
service.serve_forever()
181
52
class TestPutMessage(TestCase):
183
54
def test_put_message(self):
184
client = GrackleClient('localhost', 8420)
185
message_archives = {'arch1': []}
186
with ForkedFakeService.from_client(client, message_archives):
187
client.put_message('arch1', 'id1', StringIO('This is a message'))
188
response = client.get_messages('arch1')
189
self.assertEqual(1, len(response['messages']))
190
message = response['messages'][0]
191
self.assertEqual('id1', message['message_id'])
193
def test_put_message_without_archive(self):
194
client = GrackleClient('localhost', 8421)
195
message_archives = {'arch1': []}
196
with ForkedFakeService.from_client(client, message_archives):
55
with Forked(run_service):
56
client.put_message('arch1', StringIO('This is a message'))
197
57
with ExpectedException(Exception, 'wtf'):
198
client.put_message('no-archive', 'id1', StringIO('message'))
201
class TestGetMessages(TestCase):
203
def assertIDOrder(self, ids, messages):
204
self.assertEqual(ids, [m['message_id'] for m in messages])
206
def assertMessageIDs(self, ids, messages):
208
sorted(ids), sorted(messages, key=lambda m: m['message_id']))
210
def test_get_messages(self):
211
client = GrackleClient('localhost', 8430)
213
'baz': [make_message('foo'), make_message('bar')]}
214
with ForkedFakeService.from_client(client, archive):
215
response = client.get_messages('baz')
216
self.assertEqual(['bar', 'foo'], sorted(m['message_id'] for m in
217
response['messages']))
218
self.assertIs(None, response['next_memo'])
219
self.assertIs(None, response['previous_memo'])
221
def test_get_messages_by_id(self):
222
client = GrackleClient('localhost', 8437)
224
'baz': [make_message('foo'), make_message('bar')]}
225
with ForkedFakeService.from_client(client, archive):
226
response = client.get_messages('baz', message_ids=['foo'])
227
message, = response['messages']
228
self.assertEqual('foo', message['message_id'])
230
def test_get_messages_batching(self):
231
client = GrackleClient('localhost', 8438)
232
archive = {'baz': [make_message('foo'), make_message('bar')]}
233
with ForkedFakeService.from_client(client, archive):
234
response = client.get_messages('baz', limit=1)
235
self.assertEqual(1, len(response['messages']))
236
messages = response['messages']
237
response = client.get_messages(
238
'baz', limit=1, memo=response['next_memo'])
239
self.assertEqual(1, len(response['messages']))
240
messages.extend(response['messages'])
241
self.assertMessageIDs(['foo', 'bar'], messages)
243
def get_messages_member_order_test(self, key):
244
client = GrackleClient('localhost', 8439)
247
make_message('foo', headers={key: '2011-03-25'}),
248
make_message('bar', headers={key: '2011-03-24'}),
250
with ForkedFakeService.from_client(client, archive):
251
response = client.get_messages('baz')
252
self.assertIDOrder(['foo', 'bar'], response['messages'])
253
response = client.get_messages('baz', order=key)
254
self.assertIDOrder(['bar', 'foo'], response['messages'])
256
def test_get_messages_date_order(self):
257
self.get_messages_member_order_test('date')
259
def test_get_messages_author_order(self):
260
self.get_messages_member_order_test('author')
262
def test_get_messages_subject_order(self):
263
self.get_messages_member_order_test('subject')
265
def test_get_messages_thread_subject_order(self):
268
make_message('bar', headers={'subject': 'y'}),
269
make_message('qux', headers={'subject': 'z'}),
270
make_message('foo', headers={'subject': 'x',
271
'in-reply-to': 'qux'}),
273
client = GrackleClient('localhost', 8439)
274
with ForkedFakeService.from_client(client, archive):
275
response = client.get_messages('baz')
276
self.assertIDOrder(['bar', 'qux', 'foo'], response['messages'])
277
response = client.get_messages('baz', order='subject')
278
self.assertIDOrder(['foo', 'bar', 'qux'], response['messages'])
279
response = client.get_messages('baz', order='thread_subject')
280
self.assertIDOrder(['bar', 'qux', 'foo'], response['messages'])
282
def test_get_messages_thread_oldest_order(self):
283
client = GrackleClient('localhost', 8439)
286
make_message('bar', headers={'date': 'x'}),
287
make_message('qux', headers={'date': 'z'}),
288
make_message('foo', headers={'date': 'y',
289
'in-reply-to': 'qux'}),
291
with ForkedFakeService.from_client(client, archive):
292
response = client.get_messages('baz')
293
self.assertIDOrder(['bar', 'qux', 'foo'], response['messages'])
294
response = client.get_messages('baz', order='date')
295
self.assertIDOrder(['bar', 'foo', 'qux'], response['messages'])
296
response = client.get_messages('baz', order='thread_oldest')
297
self.assertIDOrder(['bar', 'qux', 'foo'], response['messages'])
299
def test_get_messages_thread_newest_order(self):
300
client = GrackleClient('localhost', 8439)
303
make_message('bar', headers={'date': 'x'}),
304
make_message('qux', headers={'date': 'w'}),
305
make_message('foo', headers={'date': 'y',
306
'in-reply-to': 'bar'}),
307
make_message('baz', headers={'date': 'z',
308
'in-reply-to': 'qux'}),
310
with ForkedFakeService.from_client(client, archive):
311
response = client.get_messages('baz', order='date')
313
['qux', 'bar', 'foo', 'baz'], response['messages'])
314
response = client.get_messages('baz', order='thread_newest')
316
['bar', 'foo', 'qux', 'baz'], response['messages'])
318
def test_get_messages_unsupported_order(self):
319
client = GrackleClient('localhost', 8439)
322
make_message('foo', headers={'date': '2011-03-25'}),
323
make_message('foo', headers={'date': '2011-03-24'}),
325
with ForkedFakeService.from_client(client, archive):
326
with ExpectedException(UnsupportedOrder, ''):
327
client.get_messages('baz', order='nonsense')
329
def test_get_messages_headers_no_headers(self):
330
client = GrackleClient('localhost', 8440)
331
archive = {'baz': [make_message('foo')]}
332
with ForkedFakeService.from_client(client, archive):
333
response = client.get_messages('baz', headers=[
334
'Subject', 'Date', 'X-Launchpad-Message-Rationale'])
335
first_message = response['messages'][0]
336
self.assertEqual('foo', first_message['message_id'])
337
self.assertEqual({}, first_message['headers'])
339
def test_get_messages_headers_exclude_headers(self):
340
client = GrackleClient('localhost', 8441)
342
'baz': [make_message('foo', headers={'From': 'me'})]}
343
with ForkedFakeService.from_client(client, archive):
344
response = client.get_messages('baz', headers=[
345
'Subject', 'Date', 'X-Launchpad-Message-Rationale'])
346
first_message = response['messages'][0]
347
self.assertEqual('foo', first_message['message_id'])
348
self.assertEqual({}, first_message['headers'])
350
def test_get_messages_headers_include_headers(self):
351
client = GrackleClient('localhost', 8442)
354
make_message('foo', headers={'From': 'me', 'To': 'you'})]}
355
with ForkedFakeService.from_client(client, archive):
356
response = client.get_messages('baz', headers=[
358
first_message = response['messages'][0]
359
self.assertEqual('foo', first_message['message_id'])
360
self.assertEqual({'From': 'me', 'To': 'you'}, first_message['headers'])
362
def test_get_messages_max_body_length(self):
363
client = GrackleClient('localhost', 8443)
364
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
365
with ForkedFakeService.from_client(client, archive):
366
response = client.get_messages('baz', max_body_length=3)
367
first_message = response['messages'][0]
368
self.assertEqual('abc', first_message['body'])
370
def test_include_hidden(self):
371
client = GrackleClient('localhost', 8444)
374
make_message('foo', hidden=True),
375
make_message('bar', hidden=False),
377
with ForkedFakeService.from_client(client, archive):
378
response = client.get_messages('baz', include_hidden=True)
379
self.assertMessageIDs(['bar', 'foo'], response['messages'])
380
response = client.get_messages('baz', include_hidden=False)
381
self.assertMessageIDs(['bar'], response['messages'])
383
def test_display_type_unknown_value(self):
384
client = GrackleClient('localhost', 8445)
385
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
386
with ForkedFakeService.from_client(client, archive):
387
with ExpectedException(UnsupportedDisplayType, ''):
388
client.get_messages('baz', display_type='unknown')
390
def test_display_type_headers_only(self):
391
client = GrackleClient('localhost', 8446)
394
make_message('foo', body=u'abcdefghi',
395
headers={'From': 'me', 'To': 'you'})]}
396
with ForkedFakeService.from_client(client, archive):
397
response = client.get_messages('baz', display_type='headers-only')
398
first_message = response['messages'][0]
399
self.assertEqual('foo', first_message['message_id'])
401
{'From': 'me', 'Message-Id': 'foo', 'To': 'you'},
402
first_message['headers'])
403
self.assertNotIn('body', first_message)
405
def test_display_type_text_only(self):
406
client = GrackleClient('localhost', 8446)
411
headers={'From': 'me', 'To': 'you'},
412
attachment_type='text/x-diff')]}
413
with ForkedFakeService.from_client(client, archive):
414
response = client.get_messages('baz', display_type='text-only')
415
first_message = response['messages'][0]
416
self.assertEqual('foo', first_message['message_id'])
417
self.assertEqual('me', first_message['headers']['From'])
418
self.assertEqual('you', first_message['headers']['To'])
419
self.assertEqual(archive['baz'][0]['body'], first_message['body'])
421
def test_display_type_all(self):
422
client = GrackleClient('localhost', 8447)
427
headers={'From': 'me', 'To': 'you'},
428
attachment_type='text/x-diff')]}
429
with ForkedFakeService.from_client(client, archive):
430
response = client.get_messages('baz', display_type='all')
431
first_message = response['messages'][0]
432
self.assertEqual('foo', first_message['message_id'])
433
self.assertEqual('me', first_message['headers']['From'])
434
self.assertEqual('you', first_message['headers']['To'])
435
self.assertEqual(archive['baz'][0]['body'], first_message['body'])
437
def test_date_range(self):
438
client = GrackleClient('localhost', 8448)
442
'foo', 'abcdefghi', headers={'date': '2011-12-31'}),
444
'bar', 'abcdefghi', headers={'date': '2012-01-01'}),
446
'qux', 'abcdefghi', headers={'date': '2012-01-15'}),
448
'naf', 'abcdefghi', headers={'date': '2012-01-31'}),
450
'doh', 'abcdefghi', headers={'date': '2012-02-01'}),
452
with ForkedFakeService.from_client(client, archive):
453
response = client.get_messages(
454
'baz', date_range='2012-01-01..2012-01-31')
455
ids = sorted(m['message_id'] for m in response['messages'])
456
self.assertEqual(['bar', 'naf', 'qux'], ids)
458
def test_date_range_unparsabledaterange(self):
459
client = GrackleClient('localhost', 8449)
460
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
461
with ForkedFakeService.from_client(client, archive):
462
with ExpectedException(UnparsableDateRange, ''):
463
client.get_messages('baz', date_range='2012-01-01')
465
def test_date_range_unparsabledaterange_missing_part(self):
466
client = GrackleClient('localhost', 8450)
467
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
468
with ForkedFakeService.from_client(client, archive):
469
with ExpectedException(UnparsableDateRange, ''):
470
client.get_messages('baz', date_range='2012-01-01..')
472
def test_date_range_unparsabledaterange_extra_part(self):
473
client = GrackleClient('localhost', 8451)
474
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
475
with ForkedFakeService.from_client(client, archive):
476
with ExpectedException(UnparsableDateRange, ''):
477
client.get_messages('baz', date_range='2012-01..12-02..12-03')
58
client.put_message('arch1', StringIO('This is not a message'))