3
3
BaseHTTPRequestHandler,
5
from email.message import Message
6
from email.mime.multipart import MIMEMultipart
7
from email.mime.text import MIMEText
7
11
from signal import SIGKILL
9
13
from StringIO import StringIO
10
15
from unittest import TestCase
11
16
from urlparse import urlparse
12
from urlparse import parse_qs
14
18
from testtools import ExpectedException
16
from grackle.client import (
20
from grackle.client import GrackleClient
21
from grackle.error import (
24
UnsupportedDisplayType,
22
def threaded_messages(messages):
26
for message in messages:
27
if message.get('in_reply_to') is None:
28
threads[message['message_id']] = [message]
31
pending.append(message)
32
for message in pending:
33
threads[message['in_reply_to']].append(message)
34
return threads.values()
39
def __init__(self, port, messages=None):
27
from grackle.service import ForkedFakeService
28
from grackle.store import (
34
def make_message(message_id, body='body', headers=None, hidden=False):
38
'Message-Id': message_id,
44
message_headers.update(headers.items())
46
message.set_payload(body)
47
for key, value in message_headers.items():
49
return make_json_message(message_id, message.as_string(), hidden)
52
def make_mime_message(message_id, body='body', headers=None, hidden=False,
53
attachment_type=None):
54
parts = MIMEMultipart()
55
parts.attach(MIMEText(body))
56
if attachment_type is not None:
57
attachment = Message()
58
attachment.set_payload('attactment data.')
59
attachment['Content-Type'] = attachment_type
60
attachment['Content-Disposition'] = 'attachment; filename="file.ext"'
61
parts.attach(attachment)
62
return make_message(message_id, parts.as_string(), headers, hidden)
65
class XXXForkedFakeService:
66
"""A Grackle service fake, as a ContextManager."""
68
def __init__(self, port, message_archives=None, write_logs=False):
71
:param port: The tcp port to use.
72
:param message_archives: A dict of lists of dicts representing
73
archives of messages. The outer dict represents the archive,
74
the list represents the list of messages for that archive.
75
:param write_logs: If true, log messages will be written to stdout.
42
self.messages = messages
79
if message_archives is None:
80
self.message_archives = {}
82
self.message_archives = message_archives
43
83
self.read_end, self.write_end = os.pipe()
84
self.write_logs = write_logs
87
def from_client(client, message_archives=None):
88
"""Instantiate a ForkedFakeService from the client.
90
:param port: The client to provide service for.
91
:param message_archives: A dict of lists of dicts representing
92
archives of messages. The outer dict represents the archive,
93
the list represents the list of messages for that archive.
95
return ForkedFakeService(client.port, message_archives)
45
97
def is_ready(self):
98
"""Tell the parent process that the server is ready for writes."""
46
99
os.write(self.write_end, 'asdf')
48
101
def __enter__(self):
104
Fork and start a server in the child. Return when the server is ready
51
108
self.start_server()
56
113
def start_server(self):
114
"""Start the HTTP server."""
57
115
service = HTTPServer(('', self.port), FakeGrackleRequestHandler)
58
service.messages = self.messages
116
service.store = MemoryStore(self.message_archives)
120
stream=sys.stderr, level=logging.INFO)
60
121
service.serve_forever()
62
123
def __exit__(self, exc_type, exc_val, traceback):
63
124
os.kill(self.pid, SIGKILL)
66
SUPPORTED_ORDERS = set(
67
['date', 'author', 'subject', 'thread_newest', 'thread_oldest',
71
127
class FakeGrackleRequestHandler(BaseHTTPRequestHandler):
128
"""A request handler that forwards to server.store."""
130
def __init__(self, *args, **kwargs):
131
"""Constructor. Sets up logging."""
132
self.logger = logging.getLogger('http')
133
BaseHTTPRequestHandler.__init__(self, *args, **kwargs)
136
"""Create an archive or message on PUT."""
137
scheme, netloc, path, params, query_string, fragments = (
139
parts = path.split('/')
140
if parts[1] != 'archive':
141
# This is an unknonwn operation?
144
# This expected path is /archive/archive_id.
146
self.server.store.put_archive(parts[2])
147
self.send_response(httplib.CREATED)
150
except Exception, error:
152
httplib.BAD_REQUEST, error.__doc__)
154
# This expected path is /archive/archive_id/message_id.
156
message = self.rfile.read(int(self.headers['content-length']))
157
self.server.store.put_message(parts[2], parts[3], message)
158
self.send_response(httplib.CREATED)
162
self.send_error(httplib.BAD_REQUEST)
73
164
def do_POST(self):
74
message = self.rfile.read(int(self.headers['content-length']))
75
if message == 'This is a message':
76
self.send_response(httplib.CREATED)
80
self.send_error(httplib.BAD_REQUEST)
165
"""Change a message on POST."""
166
scheme, netloc, path, params, query_string, fragments = (
168
parts = path.split('/')
169
if parts[1] != 'archive':
170
# This is an unknonwn operation?
173
# This expected path is /archive/archive_id/message_id.
175
# This expected path is /archive/archive_id/message_id.
176
response = self.server.store.hide_message(
177
parts[2], parts[3], query_string)
178
self.send_response(httplib.OK)
180
self.wfile.write(simplejson.dumps(response))
182
self.send_error(httplib.BAD_REQUEST)
185
"""Retrieve a list of messages on GET."""
83
186
scheme, netloc, path, params, query_string, fragments = (
84
187
urlparse(self.path))
85
archive = os.path.split(path)[1]
86
query = parse_qs(query_string)
87
parameters = simplejson.loads(query['parameters'][0])
88
order = parameters.get('order')
89
messages = self.server.messages[archive]
90
if order is not None :
91
if order not in SUPPORTED_ORDERS:
92
self.send_response(httplib.BAD_REQUEST)
93
self.wfile.write('Unsupported order')
188
parts = path.split('/')
189
if parts[1] == 'archive':
191
response = self.server.store.get_messages(
192
parts[2], query_string)
193
self.send_response(httplib.OK)
195
self.wfile.write(simplejson.dumps(response))
196
except Exception, error:
198
httplib.BAD_REQUEST, error.__doc__)
95
elif order.startswith('thread_'):
96
threaded = threaded_messages(messages)
98
if order == 'thread_subject':
99
threaded.sort(key=lambda t: t[0]['subject'])
100
if order == 'thread_oldest':
101
threaded.sort(key=lambda t: min(m['date'] for m in t))
102
if order == 'thread_newest':
103
threaded.sort(key=lambda t: max(m['date'] for m in t))
104
for thread in threaded:
105
messages.extend(thread)
107
messages.sort(key=lambda m: m[order])
108
messages = [m for m in messages
109
if 'message_ids' not in parameters or
110
m['message_id'] in parameters['message_ids']]
111
self.send_response(httplib.OK)
113
limit = parameters.get('limit', 100)
114
memo = parameters.get('memo')
115
message_id_indices = dict(
116
(m['message_id'], idx) for idx, m in enumerate(messages))
120
start = message_id_indices[memo.encode('rot13')]
122
previous_memo = messages[start - 1]['message_id'].encode('rot13')
125
end = min(start + limit, len(messages))
126
if end < len(messages):
127
next_memo = messages[end]['message_id'].encode('rot13')
130
messages = messages[start:end]
132
'messages': messages,
133
'next_memo': next_memo,
134
'previous_memo': previous_memo
136
self.wfile.write(simplejson.dumps(response))
139
def fake_grackle_service(client, messages=None):
142
return ForkedFake(client.port, messages)
201
def log_message(self, format, *args):
202
"""Override log_message to use standard Python logging."""
203
message = "%s - - [%s] %s\n" % (
204
self.address_string(), self.log_date_time_string(), format % args)
205
self.logger.info(message)
208
class TestPutArchive(TestCase):
210
def test_put_archive(self):
211
client = GrackleClient('localhost', 8410)
212
message_archives = {}
213
with ForkedFakeService.from_client(client, message_archives):
214
client.put_archive('arch1')
215
response = client.get_messages('arch1')
216
self.assertEqual(0, len(response['messages']))
218
def test_put_archive_existing_archive(self):
219
client = GrackleClient('localhost', 8411)
220
message_archives = {'arch1': []}
221
with ForkedFakeService.from_client(client, message_archives):
222
with ExpectedException(ArchiveIdExists, ''):
223
client.put_archive('arch1')
145
226
class TestPutMessage(TestCase):
147
228
def test_put_message(self):
148
client = GrackleClient('localhost', 8436)
149
with fake_grackle_service(client):
150
client.put_message('arch1', 'asdf', StringIO('This is a message'))
229
client = GrackleClient('localhost', 8420)
230
message_archives = {'arch1': []}
231
with ForkedFakeService.from_client(client, message_archives):
232
client.put_message('arch1', 'id1', StringIO('This is a message'))
233
response = client.get_messages('arch1')
234
self.assertEqual(1, len(response['messages']))
235
message = response['messages'][0]
236
self.assertEqual('id1', message['message_id'])
238
def test_put_message_without_archive(self):
239
client = GrackleClient('localhost', 8421)
240
message_archives = {'arch1': []}
241
with ForkedFakeService.from_client(client, message_archives):
151
242
with ExpectedException(Exception, 'wtf'):
152
client.put_message('arch1', 'asdf',
153
StringIO('This is not a message'))
243
client.put_message('no-archive', 'id1', StringIO('message'))
156
246
class TestGetMessages(TestCase):
264
367
def test_get_messages_unsupported_order(self):
265
368
client = GrackleClient('localhost', 8439)
266
with fake_grackle_service(client,
267
{'baz': [{'message_id': 'foo', 'date': '2011-03-25'},
268
{'message_id': 'bar', 'date': '2011-03-24'}]}):
269
with ExpectedException(UnsupportedOrder):
371
make_message('foo', headers={'date': '2011-03-25'}),
372
make_message('foo', headers={'date': '2011-03-24'}),
374
with ForkedFakeService.from_client(client, archive):
375
with ExpectedException(UnsupportedOrder, ''):
270
376
client.get_messages('baz', order='nonsense')
378
def test_get_messages_headers_no_headers(self):
379
client = GrackleClient('localhost', 8440)
380
archive = {'baz': [make_message('foo')]}
381
with ForkedFakeService.from_client(client, archive):
382
response = client.get_messages('baz', headers=[
383
'Subject', 'Date', 'X-Launchpad-Message-Rationale'])
384
first_message = response['messages'][0]
385
self.assertEqual('foo', first_message['message_id'])
386
self.assertEqual({}, first_message['headers'])
388
def test_get_messages_headers_exclude_headers(self):
389
client = GrackleClient('localhost', 8441)
391
'baz': [make_message('foo', headers={'From': 'me'})]}
392
with ForkedFakeService.from_client(client, archive):
393
response = client.get_messages('baz', headers=[
394
'Subject', 'Date', 'X-Launchpad-Message-Rationale'])
395
first_message = response['messages'][0]
396
self.assertEqual('foo', first_message['message_id'])
397
self.assertEqual({}, first_message['headers'])
399
def test_get_messages_headers_include_headers(self):
400
client = GrackleClient('localhost', 8442)
403
make_message('foo', headers={'From': 'me', 'To': 'you'})]}
404
with ForkedFakeService.from_client(client, archive):
405
response = client.get_messages('baz', headers=[
407
first_message = response['messages'][0]
408
self.assertEqual('foo', first_message['message_id'])
409
self.assertEqual({'From': 'me', 'To': 'you'}, first_message['headers'])
411
def test_get_messages_max_body_length(self):
412
client = GrackleClient('localhost', 8443)
413
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
414
with ForkedFakeService.from_client(client, archive):
415
response = client.get_messages('baz', max_body_length=3)
416
first_message = response['messages'][0]
417
self.assertEqual('abc', first_message['body'])
419
def test_include_hidden(self):
420
client = GrackleClient('localhost', 8444)
423
make_message('foo', hidden=True),
424
make_message('bar', hidden=False),
426
with ForkedFakeService.from_client(client, archive):
427
response = client.get_messages('baz', include_hidden=True)
428
self.assertMessageIDs(['bar', 'foo'], response['messages'])
429
response = client.get_messages('baz', include_hidden=False)
430
self.assertMessageIDs(['bar'], response['messages'])
432
def test_display_type_unknown_value(self):
433
client = GrackleClient('localhost', 8445)
434
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
435
with ForkedFakeService.from_client(client, archive):
436
with ExpectedException(UnsupportedDisplayType, ''):
437
client.get_messages('baz', display_type='unknown')
439
def test_display_type_headers_only(self):
440
client = GrackleClient('localhost', 8446)
443
make_message('foo', body=u'abcdefghi',
444
headers={'From': 'me', 'To': 'you'})]}
445
with ForkedFakeService.from_client(client, archive):
446
response = client.get_messages('baz', display_type='headers-only')
447
first_message = response['messages'][0]
448
self.assertEqual('foo', first_message['message_id'])
450
archive['baz'][0]['headers'], first_message['headers'])
451
self.assertNotIn('body', first_message)
453
def test_display_type_text_only(self):
454
client = GrackleClient('localhost', 8446)
459
headers={'From': 'me', 'To': 'you'},
460
attachment_type='text/x-diff')]}
461
with ForkedFakeService.from_client(client, archive):
462
response = client.get_messages('baz', display_type='text-only')
463
first_message = response['messages'][0]
464
self.assertEqual('foo', first_message['message_id'])
465
self.assertEqual('me', first_message['headers']['From'])
466
self.assertEqual('you', first_message['headers']['To'])
467
self.assertEqual(archive['baz'][0]['body'], first_message['body'])
469
def test_display_type_all(self):
470
client = GrackleClient('localhost', 8447)
475
headers={'From': 'me', 'To': 'you'},
476
attachment_type='text/x-diff')]}
477
with ForkedFakeService.from_client(client, archive):
478
response = client.get_messages('baz', display_type='all')
479
first_message = response['messages'][0]
480
self.assertEqual('foo', first_message['message_id'])
481
self.assertEqual('me', first_message['headers']['From'])
482
self.assertEqual('you', first_message['headers']['To'])
483
self.assertEqual(archive['baz'][0]['body'], first_message['body'])
485
def test_date_range(self):
486
client = GrackleClient('localhost', 8448)
490
'foo', 'abcdefghi', headers={'date': '2011-12-31'}),
492
'bar', 'abcdefghi', headers={'date': '2012-01-01'}),
494
'qux', 'abcdefghi', headers={'date': '2012-01-15'}),
496
'naf', 'abcdefghi', headers={'date': '2012-01-31'}),
498
'doh', 'abcdefghi', headers={'date': '2012-02-01'}),
500
with ForkedFakeService.from_client(client, archive):
501
response = client.get_messages(
502
'baz', date_range='2012-01-01..2012-01-31')
503
ids = sorted(m['message_id'] for m in response['messages'])
504
self.assertEqual(['bar', 'naf', 'qux'], ids)
506
def test_date_range_unparsabledaterange(self):
507
client = GrackleClient('localhost', 8449)
508
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
509
with ForkedFakeService.from_client(client, archive):
510
with ExpectedException(UnparsableDateRange, ''):
511
client.get_messages('baz', date_range='2012-01-01')
513
def test_date_range_unparsabledaterange_missing_part(self):
514
client = GrackleClient('localhost', 8450)
515
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
516
with ForkedFakeService.from_client(client, archive):
517
with ExpectedException(UnparsableDateRange, ''):
518
client.get_messages('baz', date_range='2012-01-01..')
520
def test_date_range_unparsabledaterange_extra_part(self):
521
client = GrackleClient('localhost', 8451)
522
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
523
with ForkedFakeService.from_client(client, archive):
524
with ExpectedException(UnparsableDateRange, ''):
525
client.get_messages('baz', date_range='2012-01..12-02..12-03')
528
class TestHideMessages(TestCase):
530
def test_hide_message_true(self):
531
client = GrackleClient('localhost', 8470)
534
make_message('foo', hidden=False),
536
with ForkedFakeService.from_client(client, archive):
537
response = client.hide_message('baz', 'foo', hidden=True)
538
self.assertEqual('foo', response['message_id'])
539
self.assertIs(True, response['hidden'])
541
def test_hide_message_false(self):
542
client = GrackleClient('localhost', 8470)
545
make_message('foo', hidden=True),
547
with ForkedFakeService.from_client(client, archive):
548
response = client.hide_message('baz', 'foo', hidden=False)
549
self.assertEqual('foo', response['message_id'])
550
self.assertIs(False, response['hidden'])