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):
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.
13
from grackle import client
18
def __init__(self, func_or_method):
19
self.func_or_method = func_or_method
77
if message_archives is None:
78
self.message_archives = {}
80
self.message_archives = message_archives
81
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)
96
"""Tell the parent process that the server is ready for writes."""
97
os.write(self.write_end, 'asdf')
99
22
def __enter__(self):
102
Fork and start a server in the child. Return when the server is ready
108
os.read(self.read_end, 1)
111
def start_server(self):
112
"""Start the HTTP server."""
113
service = HTTPServer(('', self.port), FakeGrackleRequestHandler)
114
service.store = MemoryStore(self.message_archives)
115
for archive_id, messages in service.store.message_archives.iteritems():
116
for message in messages:
117
message.setdefault('headers', {})
121
stream=sys.stderr, level=logging.INFO)
122
service.serve_forever()
124
30
def __exit__(self, exc_type, exc_val, traceback):
125
31
os.kill(self.pid, SIGKILL)
128
34
class FakeGrackleRequestHandler(BaseHTTPRequestHandler):
129
"""A request handler that forwards to server.store."""
131
def __init__(self, *args, **kwargs):
132
"""Constructor. Sets up logging."""
133
self.logger = logging.getLogger('http')
134
BaseHTTPRequestHandler.__init__(self, *args, **kwargs)
136
36
def do_POST(self):
137
"""Create a message on POST."""
138
37
message = self.rfile.read(int(self.headers['content-length']))
139
scheme, netloc, path, params, query_string, fragments = (
141
parts = path.split('/')
142
if parts[1] == 'archive' and len(parts) == 4:
144
# This expected path is /archive/archive_id/message_id.
145
self.server.store.put_message(parts[2], parts[3], message)
146
self.send_response(httplib.CREATED)
150
self.send_error(httplib.BAD_REQUEST)
153
"""Retrieve a list of messages on GET."""
154
scheme, netloc, path, params, query_string, fragments = (
156
parts = path.split('/')
157
if parts[1] == 'archive':
159
response = self.server.store.get_messages(
160
parts[2], query_string)
161
self.send_response(httplib.OK)
163
self.wfile.write(simplejson.dumps(response))
164
except Exception, error:
166
httplib.BAD_REQUEST, error.__doc__)
169
def log_message(self, format, *args):
170
"""Override log_message to use standard Python logging."""
171
message = "%s - - [%s] %s\n" % (
172
self.address_string(), self.log_date_time_string(), format % args)
173
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()
176
52
class TestPutMessage(TestCase):
178
54
def test_put_message(self):
179
client = GrackleClient('localhost', 8420)
180
message_archives = {'arch1': []}
181
with ForkedFakeService.from_client(client, message_archives):
182
client.put_message('arch1', 'id1', StringIO('This is a message'))
183
response = client.get_messages('arch1')
184
self.assertEqual(1, len(response['messages']))
185
message = response['messages'][0]
186
self.assertEqual('id1', message['message_id'])
188
def test_put_message_without_archive(self):
189
client = GrackleClient('localhost', 8421)
190
message_archives = {'arch1': []}
191
with ForkedFakeService.from_client(client, message_archives):
55
with Forked(run_service):
56
client.put_message('arch1', StringIO('This is a message'))
192
57
with ExpectedException(Exception, 'wtf'):
193
client.put_message('no-archive', 'id1', StringIO('message'))
196
class TestGetMessages(TestCase):
198
def assertIDOrder(self, ids, messages):
199
self.assertEqual(ids, [m['message_id'] for m in messages])
201
def assertMessageIDs(self, ids, messages):
203
sorted(ids), sorted(messages, key=lambda m: m['message_id']))
205
def test_get_messages(self):
206
client = GrackleClient('localhost', 8430)
208
'baz': [make_message('foo'), make_message('bar')]}
209
with ForkedFakeService.from_client(client, archive):
210
response = client.get_messages('baz')
211
self.assertEqual(['bar', 'foo'], sorted(m['message_id'] for m in
212
response['messages']))
213
self.assertIs(None, response['next_memo'])
214
self.assertIs(None, response['previous_memo'])
216
def test_get_messages_by_id(self):
217
client = GrackleClient('localhost', 8437)
219
'baz': [make_message('foo'), make_message('bar')]}
220
with ForkedFakeService.from_client(client, archive):
221
response = client.get_messages('baz', message_ids=['foo'])
222
message, = response['messages']
223
self.assertEqual('foo', message['message_id'])
225
def test_get_messages_batching(self):
226
client = GrackleClient('localhost', 8438)
227
archive = {'baz': [make_message('foo'), make_message('bar')]}
228
with ForkedFakeService.from_client(client, archive):
229
response = client.get_messages('baz', limit=1)
230
self.assertEqual(1, len(response['messages']))
231
messages = response['messages']
232
response = client.get_messages(
233
'baz', limit=1, memo=response['next_memo'])
234
self.assertEqual(1, len(response['messages']))
235
messages.extend(response['messages'])
236
self.assertMessageIDs(['foo', 'bar'], messages)
238
def get_messages_member_order_test(self, key):
239
client = GrackleClient('localhost', 8439)
246
make_message('foo', headers={header_name: '2011-03-25'}),
247
make_message('bar', headers={header_name: '2011-03-24'}),
249
with ForkedFakeService.from_client(client, archive):
250
response = client.get_messages('baz')
251
self.assertIDOrder(['foo', 'bar'], response['messages'])
252
response = client.get_messages('baz', order=key)
253
self.assertIDOrder(['bar', 'foo'], response['messages'])
255
def test_get_messages_date_order(self):
256
self.get_messages_member_order_test('date')
258
def test_get_messages_author_order(self):
259
self.get_messages_member_order_test('author')
261
def test_get_messages_subject_order(self):
262
self.get_messages_member_order_test('subject')
264
def test_get_messages_thread_subject_order(self):
267
make_message('bar', headers={'subject': 'y'}),
268
make_message('qux', headers={'subject': 'z'}),
269
make_message('foo', headers={'subject': 'x',
270
'in-reply-to': 'qux'}),
272
client = GrackleClient('localhost', 8439)
273
with ForkedFakeService.from_client(client, archive):
274
response = client.get_messages('baz')
275
self.assertIDOrder(['bar', 'qux', 'foo'], response['messages'])
276
response = client.get_messages('baz', order='subject')
277
self.assertIDOrder(['foo', 'bar', 'qux'], response['messages'])
278
response = client.get_messages('baz', order='thread_subject')
279
self.assertIDOrder(['bar', 'qux', 'foo'], response['messages'])
281
def test_get_messages_thread_oldest_order(self):
282
client = GrackleClient('localhost', 8439)
285
make_message('bar', headers={'date': 'x'}),
286
make_message('qux', headers={'date': 'z'}),
287
make_message('foo', headers={'date': 'y',
288
'in-reply-to': 'qux'}),
290
with ForkedFakeService.from_client(client, archive):
291
response = client.get_messages('baz')
292
self.assertIDOrder(['bar', 'qux', 'foo'], response['messages'])
293
response = client.get_messages('baz', order='date')
294
self.assertIDOrder(['bar', 'foo', 'qux'], response['messages'])
295
response = client.get_messages('baz', order='thread_oldest')
296
self.assertIDOrder(['bar', 'qux', 'foo'], response['messages'])
298
def test_get_messages_thread_newest_order(self):
299
client = GrackleClient('localhost', 8439)
302
make_message('bar', headers={'date': 'x'}),
303
make_message('qux', headers={'date': 'w'}),
304
make_message('foo', headers={'date': 'y',
305
'in-reply-to': 'bar'}),
306
make_message('baz', headers={'date': 'z',
307
'in-reply-to': 'qux'}),
309
with ForkedFakeService.from_client(client, archive):
310
response = client.get_messages('baz', order='date')
312
['qux', 'bar', 'foo', 'baz'], response['messages'])
313
response = client.get_messages('baz', order='thread_newest')
315
['bar', 'foo', 'qux', 'baz'], response['messages'])
317
def test_get_messages_unsupported_order(self):
318
client = GrackleClient('localhost', 8439)
321
make_message('foo', headers={'date': '2011-03-25'}),
322
make_message('foo', headers={'date': '2011-03-24'}),
324
with ForkedFakeService.from_client(client, archive):
325
with ExpectedException(UnsupportedOrder, ''):
326
client.get_messages('baz', order='nonsense')
328
def test_get_messages_headers_no_headers(self):
329
client = GrackleClient('localhost', 8440)
330
archive = {'baz': [make_message('foo')]}
331
with ForkedFakeService.from_client(client, archive):
332
response = client.get_messages('baz', headers=[
333
'Subject', 'Date', 'X-Launchpad-Message-Rationale'])
334
first_message = response['messages'][0]
335
self.assertEqual('foo', first_message['message_id'])
336
self.assertEqual({}, first_message['headers'])
338
def test_get_messages_headers_exclude_headers(self):
339
client = GrackleClient('localhost', 8441)
341
'baz': [make_message('foo', headers={'From': 'me'})]}
342
with ForkedFakeService.from_client(client, archive):
343
response = client.get_messages('baz', headers=[
344
'Subject', 'Date', 'X-Launchpad-Message-Rationale'])
345
first_message = response['messages'][0]
346
self.assertEqual('foo', first_message['message_id'])
347
self.assertEqual({}, first_message['headers'])
349
def test_get_messages_headers_include_headers(self):
350
client = GrackleClient('localhost', 8442)
353
make_message('foo', headers={'From': 'me', 'To': 'you'})]}
354
with ForkedFakeService.from_client(client, archive):
355
response = client.get_messages('baz', headers=[
357
first_message = response['messages'][0]
358
self.assertEqual('foo', first_message['message_id'])
359
self.assertEqual({'From': 'me', 'To': 'you'}, first_message['headers'])
361
def test_get_messages_max_body_length(self):
362
client = GrackleClient('localhost', 8443)
363
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
364
with ForkedFakeService.from_client(client, archive):
365
response = client.get_messages('baz', max_body_length=3)
366
first_message = response['messages'][0]
367
self.assertEqual('abc', first_message['body'])
369
def test_include_hidden(self):
370
client = GrackleClient('localhost', 8444)
373
make_message('foo', hidden=True),
374
make_message('bar', hidden=False),
376
with ForkedFakeService.from_client(client, archive):
377
response = client.get_messages('baz', include_hidden=True)
378
self.assertMessageIDs(['bar', 'foo'], response['messages'])
379
response = client.get_messages('baz', include_hidden=False)
380
self.assertMessageIDs(['bar'], response['messages'])
382
def test_display_type_unknown_value(self):
383
client = GrackleClient('localhost', 8445)
384
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
385
with ForkedFakeService.from_client(client, archive):
386
with ExpectedException(UnsupportedDisplayType, ''):
387
client.get_messages('baz', display_type='unknown')
389
def test_display_type_headers_only(self):
390
client = GrackleClient('localhost', 8446)
393
make_message('foo', body=u'abcdefghi',
394
headers={'From': 'me', 'To': 'you'})]}
395
with ForkedFakeService.from_client(client, archive):
396
response = client.get_messages('baz', display_type='headers-only')
397
first_message = response['messages'][0]
398
self.assertEqual('foo', first_message['message_id'])
400
archive['baz'][0]['headers'], first_message['headers'])
401
self.assertNotIn('body', first_message)
403
def test_display_type_text_only(self):
404
client = GrackleClient('localhost', 8446)
409
headers={'From': 'me', 'To': 'you'},
410
attachment_type='text/x-diff')]}
411
with ForkedFakeService.from_client(client, archive):
412
response = client.get_messages('baz', display_type='text-only')
413
first_message = response['messages'][0]
414
self.assertEqual('foo', first_message['message_id'])
415
self.assertEqual('me', first_message['headers']['From'])
416
self.assertEqual('you', first_message['headers']['To'])
417
self.assertEqual(archive['baz'][0]['body'], first_message['body'])
419
def test_display_type_all(self):
420
client = GrackleClient('localhost', 8447)
425
headers={'From': 'me', 'To': 'you'},
426
attachment_type='text/x-diff')]}
427
with ForkedFakeService.from_client(client, archive):
428
response = client.get_messages('baz', display_type='all')
429
first_message = response['messages'][0]
430
self.assertEqual('foo', first_message['message_id'])
431
self.assertEqual('me', first_message['headers']['From'])
432
self.assertEqual('you', first_message['headers']['To'])
433
self.assertEqual(archive['baz'][0]['body'], first_message['body'])
435
def test_date_range(self):
436
client = GrackleClient('localhost', 8448)
440
'foo', 'abcdefghi', headers={'date': '2011-12-31'}),
442
'bar', 'abcdefghi', headers={'date': '2012-01-01'}),
444
'qux', 'abcdefghi', headers={'date': '2012-01-15'}),
446
'naf', 'abcdefghi', headers={'date': '2012-01-31'}),
448
'doh', 'abcdefghi', headers={'date': '2012-02-01'}),
450
with ForkedFakeService.from_client(client, archive):
451
response = client.get_messages(
452
'baz', date_range='2012-01-01..2012-01-31')
453
ids = sorted(m['message_id'] for m in response['messages'])
454
self.assertEqual(['bar', 'naf', 'qux'], ids)
456
def test_date_range_unparsabledaterange(self):
457
client = GrackleClient('localhost', 8449)
458
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
459
with ForkedFakeService.from_client(client, archive):
460
with ExpectedException(UnparsableDateRange, ''):
461
client.get_messages('baz', date_range='2012-01-01')
463
def test_date_range_unparsabledaterange_missing_part(self):
464
client = GrackleClient('localhost', 8450)
465
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
466
with ForkedFakeService.from_client(client, archive):
467
with ExpectedException(UnparsableDateRange, ''):
468
client.get_messages('baz', date_range='2012-01-01..')
470
def test_date_range_unparsabledaterange_extra_part(self):
471
client = GrackleClient('localhost', 8451)
472
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
473
with ForkedFakeService.from_client(client, archive):
474
with ExpectedException(UnparsableDateRange, ''):
475
client.get_messages('baz', date_range='2012-01..12-02..12-03')
58
client.put_message('arch1', StringIO('This is not a message'))