1
from BaseHTTPServer import (
3
BaseHTTPRequestHandler,
5
1
from email.message import Message
6
2
from email.mime.multipart import MIMEMultipart
7
3
from email.mime.text import MIMEText
11
from signal import SIGKILL
13
4
from StringIO import StringIO
15
5
from unittest import TestCase
16
from urlparse import urlparse
17
from urlparse import parse_qs
19
7
from testtools import ExpectedException
21
from grackle.client import (
9
from grackle.client import GrackleClient
10
from grackle.error import (
23
12
UnparsableDateRange,
24
13
UnsupportedDisplayType,
16
from grackle.service import ForkedFakeService
17
from grackle.store import (
29
22
def make_message(message_id, body='body', headers=None, hidden=False):
30
23
if headers is None:
32
headers['Message-Id'] = message_id
34
'message_id': message_id,
36
'thread_id': message_id,
37
'date': headers.get('date', '2005-01-01'),
38
'subject': headers.get('subject', 'subject'),
39
'author': headers.get('author', 'author'),
42
'replies': headers.get('in-reply-to', None),
26
'Message-Id': message_id,
32
message_headers.update(headers.items())
34
message.set_payload(body)
35
for key, value in message_headers.items():
37
return make_json_message(message_id, message.as_string(), hidden)
48
40
def make_mime_message(message_id, body='body', headers=None, hidden=False,
49
41
attachment_type=None):
50
message = MIMEMultipart()
51
message.attach(MIMEText(body))
42
parts = MIMEMultipart()
43
parts.attach(MIMEText(body))
52
44
if attachment_type is not None:
53
45
attachment = Message()
54
46
attachment.set_payload('attactment data.')
55
47
attachment['Content-Type'] = attachment_type
56
48
attachment['Content-Disposition'] = 'attachment; filename="file.ext"'
57
message.attach(attachment)
58
return make_message(message_id, message.get_payload(), headers, hidden)
61
def threaded_messages(messages):
65
for message in messages:
66
if message.get('replies') is None:
67
threads[message['message_id']] = [message]
70
pending.append(message)
71
for message in pending:
72
threads[message['replies']].append(message)
73
return threads.values()
77
"""A memory-backed message store."""
79
def __init__(self, messages):
81
self.messages = messages
84
def is_multipart(message):
85
return isinstance(message['body'], list)
87
def get_messages(self, archive_id, query_string):
88
"""Return matching messages.
90
:param archive_id: The archive to retrieve from.
91
:param query_string: Contains 'parameters', which is a JSON-format
92
string describing parameters.
94
query = parse_qs(query_string)
95
parameters = simplejson.loads(query['parameters'][0])
96
order = parameters.get('order')
97
messages = self.messages[archive_id]
99
if order not in SUPPORTED_ORDERS:
100
raise UnsupportedOrder
101
elif order.startswith('thread_'):
102
threaded = threaded_messages(messages)
104
if order == 'thread_subject':
105
threaded.sort(key=lambda t: t[0]['subject'])
106
if order == 'thread_oldest':
107
threaded.sort(key=lambda t: min(m['date'] for m in t))
108
if order == 'thread_newest':
109
threaded.sort(key=lambda t: max(m['date'] for m in t))
110
for thread in threaded:
111
messages.extend(thread)
113
messages.sort(key=lambda m: m[order])
114
display_type = parameters.get('display_type', 'all')
115
if display_type not in SUPPORTED_DISPLAY_TYPES:
116
raise UnsupportedDisplayType
117
if 'date_range' in parameters:
119
start_date, end_date = parameters['date_range'].split('..')
121
raise UnparsableDateRange
123
for message in messages:
124
if (not parameters['include_hidden'] and message['hidden']):
126
if ('message_ids' in parameters
127
and message['message_id'] not in parameters['message_ids']):
129
if ('date_range' in parameters
130
and (message['date'] < start_date
131
or message['date'] > end_date)):
133
message = dict(message)
134
if 'headers' in parameters:
136
(k, v) for k, v in message['headers'].iteritems()
137
if k in parameters['headers'])
138
message['headers'] = headers
139
if display_type == 'headers-only':
141
elif display_type == 'text-only' and self.is_multipart(message):
143
part.get_payload() for part in message['body']
144
if part.get_content_type() == 'text/plain']
145
message['body'] = '\n\n'.join(text_parts)
146
elif display_type == 'all' and self.is_multipart(message):
147
parts = [str(part.get_payload()) for part in message['body']]
148
message['body'] = '\n\n'.join(parts)
149
max_body = parameters.get('max_body_length')
150
if max_body is not None and display_type != 'headers-only':
151
message['body'] = message['body'][:max_body]
152
new_messages.append(message)
153
messages = new_messages
154
limit = parameters.get('limit', 100)
155
memo = parameters.get('memo')
156
message_id_indices = dict(
157
(m['message_id'], idx) for idx, m in enumerate(messages))
161
start = message_id_indices[memo.encode('rot13')]
163
previous_memo = messages[start - 1]['message_id'].encode('rot13')
166
end = min(start + limit, len(messages))
167
if end < len(messages):
168
next_memo = messages[end]['message_id'].encode('rot13')
171
messages = messages[start:end]
174
'messages': messages,
175
'next_memo': next_memo,
176
'previous_memo': previous_memo
181
class ForkedFakeService:
182
"""A Grackle service fake, as a ContextManager."""
184
def __init__(self, port, messages=None, write_logs=False):
187
:param port: The tcp port to use.
188
:param messages: A dict of lists of dicts representing messages. The
189
outer dict represents the archive, the list represents the list of
190
messages for that archive.
191
:param write_logs: If true, log messages will be written to stdout.
198
self.messages = messages
199
self.read_end, self.write_end = os.pipe()
200
self.write_logs = write_logs
203
def from_client(client, messages=None):
204
"""Instantiate a ForkedFakeService from the client.
206
:param port: The client to provide service for.
207
:param messages: A dict of lists of dicts representing messages. The
208
outer dict represents the archive, the list represents the list of
209
messages for that archive.
211
return ForkedFakeService(client.port, messages)
214
"""Tell the parent process that the server is ready for writes."""
215
os.write(self.write_end, 'asdf')
220
Fork and start a server in the child. Return when the server is ready
226
os.read(self.read_end, 1)
229
def start_server(self):
230
"""Start the HTTP server."""
231
service = HTTPServer(('', self.port), FakeGrackleRequestHandler)
232
service.store = GrackleStore(self.messages)
233
for archive_id, messages in service.store.messages.iteritems():
234
for message in messages:
235
message.setdefault('headers', {})
239
stream=sys.stderr, level=logging.INFO)
240
service.serve_forever()
242
def __exit__(self, exc_type, exc_val, traceback):
243
os.kill(self.pid, SIGKILL)
246
SUPPORTED_DISPLAY_TYPES = set(['all', 'text-only', 'headers-only'])
249
SUPPORTED_ORDERS = set(
250
['date', 'author', 'subject', 'thread_newest', 'thread_oldest',
254
class FakeGrackleRequestHandler(BaseHTTPRequestHandler):
255
"""A request handler that forwards to server.store."""
257
def __init__(self, *args, **kwargs):
258
"""Constructor. Sets up logging."""
259
self.logger = logging.getLogger('http')
260
BaseHTTPRequestHandler.__init__(self, *args, **kwargs)
263
"""Create a message on POST."""
264
message = self.rfile.read(int(self.headers['content-length']))
265
if message == 'This is a message':
266
self.send_response(httplib.CREATED)
270
self.send_error(httplib.BAD_REQUEST)
273
"""Retrieve a list of messages on GET."""
274
scheme, netloc, path, params, query_string, fragments = (
276
parts = path.split('/')
277
if parts[1] == 'archive':
279
response = self.server.store.get_messages(
280
parts[2], query_string)
281
self.send_response(httplib.OK)
283
self.wfile.write(simplejson.dumps(response))
284
except Exception, error:
286
httplib.BAD_REQUEST, error.__doc__)
289
def log_message(self, format, *args):
290
"""Override log_message to use standard Python logging."""
291
message = "%s - - [%s] %s\n" % (
292
self.address_string(), self.log_date_time_string(), format % args)
293
self.logger.info(message)
49
parts.attach(attachment)
50
return make_message(message_id, parts.as_string(), headers, hidden)
53
class TestPutArchive(TestCase):
55
def test_put_archive(self):
56
client = GrackleClient('localhost', 8410)
58
with ForkedFakeService.from_client(client, message_archives):
59
client.put_archive('arch1')
60
response = client.get_messages('arch1')
61
self.assertEqual(0, len(response['messages']))
63
def test_put_archive_existing_archive(self):
64
client = GrackleClient('localhost', 8411)
65
message_archives = {'arch1': []}
66
with ForkedFakeService.from_client(client, message_archives):
67
with ExpectedException(ArchiveIdExists, ''):
68
client.put_archive('arch1')
296
71
class TestPutMessage(TestCase):
298
73
def test_put_message(self):
299
client = GrackleClient('localhost', 8436)
300
with ForkedFakeService.from_client(client):
301
client.put_message('arch1', 'asdf', StringIO('This is a message'))
74
client = GrackleClient('localhost', 8420)
75
message_archives = {'arch1': []}
76
with ForkedFakeService.from_client(client, message_archives):
77
client.put_message('arch1', 'id1', StringIO('This is a message'))
78
response = client.get_messages('arch1')
79
self.assertEqual(1, len(response['messages']))
80
message = response['messages'][0]
81
self.assertEqual('id1', message['message_id'])
83
def test_put_message_without_archive(self):
84
client = GrackleClient('localhost', 8421)
85
message_archives = {'arch1': []}
86
with ForkedFakeService.from_client(client, message_archives):
302
87
with ExpectedException(Exception, 'wtf'):
303
client.put_message('arch1', 'asdf',
304
StringIO('This is not a message'))
88
client.put_message('no-archive', 'id1', StringIO('message'))
307
91
class TestGetMessages(TestCase):
563
349
self.assertEqual(['bar', 'naf', 'qux'], ids)
565
351
def test_date_range_unparsabledaterange(self):
566
client = GrackleClient('localhost', 8448)
352
client = GrackleClient('localhost', 8449)
567
353
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
568
354
with ForkedFakeService.from_client(client, archive):
569
355
with ExpectedException(UnparsableDateRange, ''):
570
356
client.get_messages('baz', date_range='2012-01-01')
358
def test_date_range_unparsabledaterange_missing_part(self):
359
client = GrackleClient('localhost', 8450)
360
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
361
with ForkedFakeService.from_client(client, archive):
362
with ExpectedException(UnparsableDateRange, ''):
363
client.get_messages('baz', date_range='2012-01-01..')
365
def test_date_range_unparsabledaterange_extra_part(self):
366
client = GrackleClient('localhost', 8451)
367
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
368
with ForkedFakeService.from_client(client, archive):
369
with ExpectedException(UnparsableDateRange, ''):
370
client.get_messages('baz', date_range='2012-01..12-02..12-03')
373
class TestHideMessages(TestCase):
375
def test_hide_message_true(self):
376
client = GrackleClient('localhost', 8470)
379
make_message('foo', hidden=False),
381
with ForkedFakeService.from_client(client, archive):
382
response = client.hide_message('baz', 'foo', hidden=True)
383
self.assertEqual('foo', response['message_id'])
384
self.assertIs(True, response['hidden'])
386
def test_hide_message_false(self):
387
client = GrackleClient('localhost', 8470)
390
make_message('foo', hidden=True),
392
with ForkedFakeService.from_client(client, archive):
393
response = client.hide_message('baz', 'foo', hidden=False)
394
self.assertEqual('foo', response['message_id'])
395
self.assertIs(False, response['hidden'])