15
12
from unittest import TestCase
16
13
from urlparse import urlparse
14
from urlparse import parse_qs
18
16
from testtools import ExpectedException
20
18
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:
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
64
121
"""A Grackle service fake, as a ContextManager."""
66
def __init__(self, port, message_archives=None, write_logs=False):
123
def __init__(self, port, messages=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.
125
:param port: The tcp port to use
126
:param messages: A dict of lists of dicts representing messages. The
127
outer dict represents the archive, the list represents the list of
128
messages for that archive.
73
129
:param write_logs: If true, log messages will be written to stdout.
77
if message_archives is None:
78
self.message_archives = {}
80
self.message_archives = message_archives
136
self.messages = messages
81
137
self.read_end, self.write_end = os.pipe()
82
138
self.write_logs = write_logs
85
def from_client(client, message_archives=None):
86
"""Instantiate a ForkedFakeService from the client.
141
def from_client(client, messages=None):
142
"""Instantiate a ForkedFake 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.
144
:param port: The client to provide service for.
145
:param messages: A dict of lists of dicts representing messages. The
146
outer dict represents the archive, the list represents the list of
147
messages for that archive.
93
return ForkedFakeService(client.port, message_archives)
149
return ForkedFake(client.port, messages)
95
151
def is_ready(self):
96
152
"""Tell the parent process that the server is ready for writes."""
133
194
self.logger = logging.getLogger('http')
134
195
BaseHTTPRequestHandler.__init__(self, *args, **kwargs)
137
"""Create an archive or message on PUT."""
138
scheme, netloc, path, params, query_string, fragments = (
140
parts = path.split('/')
141
if parts[1] != 'archive':
142
# This is an unknonwn operation?
145
# This expected path is /archive/archive_id.
147
self.server.store.put_archive(parts[2])
148
self.send_response(httplib.CREATED)
152
self.send_error(httplib.BAD_REQUEST)
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)
164
197
def do_POST(self):
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)
198
"""Create a message on POST."""
199
message = self.rfile.read(int(self.headers['content-length']))
200
if message == 'This is a message':
201
self.send_response(httplib.CREATED)
205
self.send_error(httplib.BAD_REQUEST)
184
207
def do_GET(self):
185
208
"""Retrieve a list of messages on GET."""
205
228
self.logger.info(message)
208
class TestPutArchive(TestCase):
210
def test_put_message(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']))
219
231
class TestPutMessage(TestCase):
221
233
def test_put_message(self):
222
client = GrackleClient('localhost', 8420)
223
message_archives = {'arch1': []}
224
with ForkedFakeService.from_client(client, message_archives):
225
client.put_message('arch1', 'id1', StringIO('This is a message'))
226
response = client.get_messages('arch1')
227
self.assertEqual(1, len(response['messages']))
228
message = response['messages'][0]
229
self.assertEqual('id1', message['message_id'])
231
def test_put_message_without_archive(self):
232
client = GrackleClient('localhost', 8421)
233
message_archives = {'arch1': []}
234
with ForkedFakeService.from_client(client, message_archives):
234
client = GrackleClient('localhost', 8436)
235
with ForkedFake.from_client(client):
236
client.put_message('arch1', 'asdf', StringIO('This is a message'))
235
237
with ExpectedException(Exception, 'wtf'):
236
client.put_message('no-archive', 'id1', StringIO('message'))
238
client.put_message('arch1', 'asdf',
239
StringIO('This is not a message'))
239
242
class TestGetMessages(TestCase):
404
394
def test_get_messages_max_body_length(self):
405
395
client = GrackleClient('localhost', 8443)
406
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
407
with ForkedFakeService.from_client(client, archive):
396
with ForkedFake.from_client(client,
398
{'message_id': 'foo', 'body': u'abcdefghi'}
408
400
response = client.get_messages('baz', max_body_length=3)
409
401
first_message = response['messages'][0]
410
402
self.assertEqual('abc', first_message['body'])
412
404
def test_include_hidden(self):
413
405
client = GrackleClient('localhost', 8444)
416
make_message('foo', hidden=True),
417
make_message('bar', hidden=False),
419
with ForkedFakeService.from_client(client, archive):
406
with ForkedFake.from_client(client,
408
{'message_id': 'foo', 'hidden': True},
409
{'message_id': 'bar', 'hidden': False}
420
411
response = client.get_messages('baz', include_hidden=True)
421
412
self.assertMessageIDs(['bar', 'foo'], response['messages'])
422
413
response = client.get_messages('baz', include_hidden=False)
423
414
self.assertMessageIDs(['bar'], response['messages'])
425
def test_display_type_unknown_value(self):
426
client = GrackleClient('localhost', 8445)
427
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
428
with ForkedFakeService.from_client(client, archive):
429
with ExpectedException(UnsupportedDisplayType, ''):
430
client.get_messages('baz', display_type='unknown')
432
def test_display_type_headers_only(self):
433
client = GrackleClient('localhost', 8446)
436
make_message('foo', body=u'abcdefghi',
437
headers={'From': 'me', 'To': 'you'})]}
438
with ForkedFakeService.from_client(client, archive):
439
response = client.get_messages('baz', display_type='headers-only')
440
first_message = response['messages'][0]
441
self.assertEqual('foo', first_message['message_id'])
443
archive['baz'][0]['headers'], first_message['headers'])
444
self.assertNotIn('body', first_message)
446
def test_display_type_text_only(self):
447
client = GrackleClient('localhost', 8446)
452
headers={'From': 'me', 'To': 'you'},
453
attachment_type='text/x-diff')]}
454
with ForkedFakeService.from_client(client, archive):
455
response = client.get_messages('baz', display_type='text-only')
456
first_message = response['messages'][0]
457
self.assertEqual('foo', first_message['message_id'])
458
self.assertEqual('me', first_message['headers']['From'])
459
self.assertEqual('you', first_message['headers']['To'])
460
self.assertEqual(archive['baz'][0]['body'], first_message['body'])
462
def test_display_type_all(self):
463
client = GrackleClient('localhost', 8447)
468
headers={'From': 'me', 'To': 'you'},
469
attachment_type='text/x-diff')]}
470
with ForkedFakeService.from_client(client, archive):
471
response = client.get_messages('baz', display_type='all')
472
first_message = response['messages'][0]
473
self.assertEqual('foo', first_message['message_id'])
474
self.assertEqual('me', first_message['headers']['From'])
475
self.assertEqual('you', first_message['headers']['To'])
476
self.assertEqual(archive['baz'][0]['body'], first_message['body'])
478
def test_date_range(self):
479
client = GrackleClient('localhost', 8448)
483
'foo', 'abcdefghi', headers={'date': '2011-12-31'}),
485
'bar', 'abcdefghi', headers={'date': '2012-01-01'}),
487
'qux', 'abcdefghi', headers={'date': '2012-01-15'}),
489
'naf', 'abcdefghi', headers={'date': '2012-01-31'}),
491
'doh', 'abcdefghi', headers={'date': '2012-02-01'}),
493
with ForkedFakeService.from_client(client, archive):
494
response = client.get_messages(
495
'baz', date_range='2012-01-01..2012-01-31')
496
ids = sorted(m['message_id'] for m in response['messages'])
497
self.assertEqual(['bar', 'naf', 'qux'], ids)
499
def test_date_range_unparsabledaterange(self):
500
client = GrackleClient('localhost', 8449)
501
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
502
with ForkedFakeService.from_client(client, archive):
503
with ExpectedException(UnparsableDateRange, ''):
504
client.get_messages('baz', date_range='2012-01-01')
506
def test_date_range_unparsabledaterange_missing_part(self):
507
client = GrackleClient('localhost', 8450)
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_extra_part(self):
514
client = GrackleClient('localhost', 8451)
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..12-02..12-03')
521
class TestHideMessages(TestCase):
523
def test_hide_message_true(self):
524
client = GrackleClient('localhost', 8470)
527
make_message('foo', hidden=False),
529
with ForkedFakeService.from_client(client, archive):
530
response = client.hide_message('baz', 'foo', hidden=True)
531
self.assertEqual('foo', response['message_id'])
532
self.assertIs(True, response['hidden'])
534
def test_hide_message_false(self):
535
client = GrackleClient('localhost', 8470)
538
make_message('foo', hidden=True),
540
with ForkedFakeService.from_client(client, archive):
541
response = client.hide_message('baz', 'foo', hidden=False)
542
self.assertEqual('foo', response['message_id'])
543
self.assertIs(False, response['hidden'])