15
15
from unittest import TestCase
16
16
from urlparse import urlparse
17
from urlparse import parse_qs
18
19
from testtools import ExpectedException
20
from grackle.client import GrackleClient
21
from grackle.error import (
21
from grackle.client import (
23
23
UnparsableDateRange,
24
24
UnsupportedDisplayType,
27
from grackle.store import (
33
29
def make_message(message_id, body='body', headers=None, hidden=False):
34
30
if headers is None:
37
'Message-Id': message_id,
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),
43
message_headers.update(headers.items())
45
message.set_payload(body)
46
for key, value in message_headers.items():
48
return make_json_message(message_id, message.as_string(), hidden)
51
48
def make_mime_message(message_id, body='body', headers=None, hidden=False,
52
49
attachment_type=None):
53
parts = MIMEMultipart()
54
parts.attach(MIMEText(body))
50
message = MIMEMultipart()
51
message.attach(MIMEText(body))
55
52
if attachment_type is not None:
56
53
attachment = Message()
57
54
attachment.set_payload('attactment data.')
58
55
attachment['Content-Type'] = attachment_type
59
56
attachment['Content-Disposition'] = 'attachment; filename="file.ext"'
60
parts.attach(attachment)
61
return make_message(message_id, parts.as_string(), headers, hidden)
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
64
181
class ForkedFakeService:
65
182
"""A Grackle service fake, as a ContextManager."""
67
def __init__(self, port, message_archives=None, write_logs=False):
184
def __init__(self, port, messages=None, write_logs=False):
70
187
:param port: The tcp port to use.
71
:param message_archives: A dict of lists of dicts representing
72
archives of messages. The outer dict represents the archive,
73
the list represents the list of messages for that archive.
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.
74
191
:param write_logs: If true, log messages will be written to stdout.
78
if message_archives is None:
79
self.message_archives = {}
81
self.message_archives = message_archives
198
self.messages = messages
82
199
self.read_end, self.write_end = os.pipe()
83
200
self.write_logs = write_logs
86
def from_client(client, message_archives=None):
203
def from_client(client, messages=None):
87
204
"""Instantiate a ForkedFakeService from the client.
89
206
:param port: The client to provide service for.
90
:param message_archives: A dict of lists of dicts representing
91
archives of messages. The outer dict represents the archive,
92
the list represents the list of messages for that archive.
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.
94
return ForkedFakeService(client.port, message_archives)
211
return ForkedFakeService(client.port, messages)
96
213
def is_ready(self):
97
214
"""Tell the parent process that the server is ready for writes."""
134
259
self.logger = logging.getLogger('http')
135
260
BaseHTTPRequestHandler.__init__(self, *args, **kwargs)
138
"""Create an archive or message on PUT."""
139
scheme, netloc, path, params, query_string, fragments = (
141
parts = path.split('/')
142
if parts[1] != 'archive':
143
# This is an unknonwn operation?
146
# This expected path is /archive/archive_id.
148
self.server.store.put_archive(parts[2])
149
self.send_response(httplib.CREATED)
152
except Exception, error:
154
httplib.BAD_REQUEST, error.__doc__)
156
# This expected path is /archive/archive_id/message_id.
158
message = self.rfile.read(int(self.headers['content-length']))
159
self.server.store.put_message(parts[2], parts[3], message)
160
self.send_response(httplib.CREATED)
164
self.send_error(httplib.BAD_REQUEST)
166
262
def do_POST(self):
167
"""Change a message on POST."""
168
scheme, netloc, path, params, query_string, fragments = (
170
parts = path.split('/')
171
if parts[1] != 'archive':
172
# This is an unknonwn operation?
175
# This expected path is /archive/archive_id/message_id.
177
# This expected path is /archive/archive_id/message_id.
178
response = self.server.store.hide_message(
179
parts[2], parts[3], query_string)
180
self.send_response(httplib.OK)
182
self.wfile.write(simplejson.dumps(response))
184
self.send_error(httplib.BAD_REQUEST)
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)
186
272
def do_GET(self):
187
273
"""Retrieve a list of messages on GET."""
207
293
self.logger.info(message)
210
class TestPutArchive(TestCase):
212
def test_put_archive(self):
213
client = GrackleClient('localhost', 8410)
214
message_archives = {}
215
with ForkedFakeService.from_client(client, message_archives):
216
client.put_archive('arch1')
217
response = client.get_messages('arch1')
218
self.assertEqual(0, len(response['messages']))
220
def test_put_archive_existing_archive(self):
221
client = GrackleClient('localhost', 8411)
222
message_archives = {'arch1': []}
223
with ForkedFakeService.from_client(client, message_archives):
224
with ExpectedException(ArchiveIdExists, ''):
225
client.put_archive('arch1')
228
296
class TestPutMessage(TestCase):
230
298
def test_put_message(self):
231
client = GrackleClient('localhost', 8420)
232
message_archives = {'arch1': []}
233
with ForkedFakeService.from_client(client, message_archives):
234
client.put_message('arch1', 'id1', StringIO('This is a message'))
235
response = client.get_messages('arch1')
236
self.assertEqual(1, len(response['messages']))
237
message = response['messages'][0]
238
self.assertEqual('id1', message['message_id'])
240
def test_put_message_without_archive(self):
241
client = GrackleClient('localhost', 8421)
242
message_archives = {'arch1': []}
243
with ForkedFakeService.from_client(client, message_archives):
299
client = GrackleClient('localhost', 8436)
300
with ForkedFakeService.from_client(client):
301
client.put_message('arch1', 'asdf', StringIO('This is a message'))
244
302
with ExpectedException(Exception, 'wtf'):
245
client.put_message('no-archive', 'id1', StringIO('message'))
303
client.put_message('arch1', 'asdf',
304
StringIO('This is not a message'))
248
307
class TestGetMessages(TestCase):
506
563
self.assertEqual(['bar', 'naf', 'qux'], ids)
508
565
def test_date_range_unparsabledaterange(self):
509
client = GrackleClient('localhost', 8449)
566
client = GrackleClient('localhost', 8448)
510
567
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
511
568
with ForkedFakeService.from_client(client, archive):
512
569
with ExpectedException(UnparsableDateRange, ''):
513
570
client.get_messages('baz', date_range='2012-01-01')
515
def test_date_range_unparsabledaterange_missing_part(self):
516
client = GrackleClient('localhost', 8450)
517
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
518
with ForkedFakeService.from_client(client, archive):
519
with ExpectedException(UnparsableDateRange, ''):
520
client.get_messages('baz', date_range='2012-01-01..')
522
def test_date_range_unparsabledaterange_extra_part(self):
523
client = GrackleClient('localhost', 8451)
524
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
525
with ForkedFakeService.from_client(client, archive):
526
with ExpectedException(UnparsableDateRange, ''):
527
client.get_messages('baz', date_range='2012-01..12-02..12-03')
530
class TestHideMessages(TestCase):
532
def test_hide_message_true(self):
533
client = GrackleClient('localhost', 8470)
536
make_message('foo', hidden=False),
538
with ForkedFakeService.from_client(client, archive):
539
response = client.hide_message('baz', 'foo', hidden=True)
540
self.assertEqual('foo', response['message_id'])
541
self.assertIs(True, response['hidden'])
543
def test_hide_message_false(self):
544
client = GrackleClient('localhost', 8470)
547
make_message('foo', hidden=True),
549
with ForkedFakeService.from_client(client, archive):
550
response = client.hide_message('baz', 'foo', hidden=False)
551
self.assertEqual('foo', response['message_id'])
552
self.assertIs(False, response['hidden'])