~didrocks/unity/altf10

« back to all changes in this revision

Viewing changes to grackle/tests/test_client.py

  • Committer: Curtis Hovey
  • Date: 2012-01-31 05:24:10 UTC
  • mfrom: (35.1.19 client-get-messages-0)
  • Revision ID: curtis.hovey@canonical.com-20120131052410-4n5iva4ujik6nhp8
Added support for display_type.
Introduced make_message and make_mime_message to make test data consistent for
InMemoryStore.

Show diffs side-by-side

added added

removed removed

Lines of Context:
2
2
    HTTPServer,
3
3
    BaseHTTPRequestHandler,
4
4
    )
 
5
from email.message import Message
 
6
from email.mime.multipart import MIMEMultipart
 
7
from email.mime.text import MIMEText
5
8
import httplib
 
9
import logging
6
10
import os
7
11
from signal import SIGKILL
 
12
import simplejson
8
13
from StringIO import StringIO
 
14
import sys
9
15
from unittest import TestCase
 
16
from urlparse import urlparse
 
17
from urlparse import parse_qs
10
18
 
11
19
from testtools import ExpectedException
12
20
 
13
21
from grackle.client import (
14
22
    GrackleClient,
 
23
    UnsupportedDisplayType,
 
24
    UnsupportedOrder,
15
25
    )
16
26
 
17
27
 
18
 
class Forked:
19
 
 
20
 
    def __init__(self, func_or_method, *args):
21
 
        self.func_or_method = func_or_method
 
28
def make_message(message_id, body='body', headers=None, hidden=False):
 
29
    if headers is None:
 
30
        headers = {}
 
31
    headers['Message-Id'] = message_id
 
32
    message = {
 
33
        'message_id': message_id,
 
34
        'headers': headers,
 
35
        'thread_id': message_id,
 
36
        'date': headers.get('date', '2005-01-01'),
 
37
        'subject': headers.get('subject', 'subject'),
 
38
        'author': headers.get('author', 'author'),
 
39
        'hidden': hidden,
 
40
        'attachments': [],
 
41
        'replies': headers.get('in-reply-to', None),
 
42
        'body': body,
 
43
        }
 
44
    return message
 
45
 
 
46
 
 
47
def make_mime_message(message_id, body='body', headers=None, hidden=False,
 
48
                      attachment_type=None):
 
49
    message = MIMEMultipart()
 
50
    message.attach(MIMEText(body))
 
51
    if attachment_type is not None:
 
52
        attachment = Message()
 
53
        attachment.set_payload('attactment data.')
 
54
        attachment['Content-Type'] = attachment_type
 
55
        attachment['Content-Disposition'] = 'attachment; filename="file.ext"'
 
56
        message.attach(attachment)
 
57
    return make_message(message_id, message.get_payload(), headers, hidden)
 
58
 
 
59
 
 
60
def threaded_messages(messages):
 
61
    threads = {}
 
62
    count = 0
 
63
    pending = []
 
64
    for message in messages:
 
65
        if message.get('replies') is None:
 
66
            threads[message['message_id']] = [message]
 
67
            count += 1
 
68
        else:
 
69
            pending.append(message)
 
70
    for message in pending:
 
71
        threads[message['replies']].append(message)
 
72
    return threads.values()
 
73
 
 
74
 
 
75
class GrackleStore:
 
76
    """A memory-backed message store."""
 
77
 
 
78
    def __init__(self, messages):
 
79
        """Constructor."""
 
80
        self.messages = messages
 
81
 
 
82
    @staticmethod
 
83
    def is_multipart(message):
 
84
        return isinstance(message['body'], list)
 
85
 
 
86
    def get_messages(self, archive_id, query_string):
 
87
        """Return matching messages.
 
88
 
 
89
        :param archive_id: The archive to retrieve from.
 
90
        :param query_string: Contains 'parameters', which is a JSON-format
 
91
            string describing parameters.
 
92
        """
 
93
        query = parse_qs(query_string)
 
94
        parameters = simplejson.loads(query['parameters'][0])
 
95
        order = parameters.get('order')
 
96
        messages = self.messages[archive_id]
 
97
        if order is not None:
 
98
            if order not in SUPPORTED_ORDERS:
 
99
                raise UnsupportedOrder
 
100
            elif order.startswith('thread_'):
 
101
                threaded = threaded_messages(messages)
 
102
                messages = []
 
103
                if order == 'thread_subject':
 
104
                    threaded.sort(key=lambda t: t[0]['subject'])
 
105
                if order == 'thread_oldest':
 
106
                    threaded.sort(key=lambda t: min(m['date'] for m in t))
 
107
                if order == 'thread_newest':
 
108
                    threaded.sort(key=lambda t: max(m['date'] for m in t))
 
109
                for thread in threaded:
 
110
                    messages.extend(thread)
 
111
            else:
 
112
                messages.sort(key=lambda m: m[order])
 
113
        display_type = parameters.get('display_type', 'all')
 
114
        if display_type not in SUPPORTED_DISPLAY_TYPES:
 
115
            raise UnsupportedDisplayType
 
116
        new_messages = []
 
117
        for message in messages:
 
118
            if (not parameters['include_hidden'] and message['hidden']):
 
119
                continue
 
120
            if ('message_ids' in parameters
 
121
                and message['message_id'] not in parameters['message_ids']):
 
122
                continue
 
123
            message = dict(message)
 
124
            if 'headers' in parameters:
 
125
                headers = dict(
 
126
                    (k, v) for k, v in message['headers'].iteritems()
 
127
                    if k in parameters['headers'])
 
128
                message['headers'] = headers
 
129
            if display_type == 'headers-only':
 
130
                del message['body']
 
131
            elif display_type == 'text-only' and self.is_multipart(message):
 
132
                text_parts = [
 
133
                    part.get_payload() for part in message['body']
 
134
                    if part.get_content_type() == 'text/plain']
 
135
                message['body'] = '\n\n'.join(text_parts)
 
136
            elif display_type == 'all' and self.is_multipart(message):
 
137
                parts = [str(part.get_payload()) for part in message['body']]
 
138
                message['body'] = '\n\n'.join(parts)
 
139
            max_body = parameters.get('max_body_length')
 
140
            if max_body is not None and display_type != 'headers-only':
 
141
                message['body'] = message['body'][:max_body]
 
142
            new_messages.append(message)
 
143
        messages = new_messages
 
144
        limit = parameters.get('limit', 100)
 
145
        memo = parameters.get('memo')
 
146
        message_id_indices = dict(
 
147
            (m['message_id'], idx) for idx, m in enumerate(messages))
 
148
        if memo is None:
 
149
            start = 0
 
150
        else:
 
151
            start = message_id_indices[memo.encode('rot13')]
 
152
        if start > 0:
 
153
            previous_memo = messages[start - 1]['message_id'].encode('rot13')
 
154
        else:
 
155
            previous_memo = None
 
156
        end = min(start + limit, len(messages))
 
157
        if end < len(messages):
 
158
            next_memo = messages[end]['message_id'].encode('rot13')
 
159
        else:
 
160
            next_memo = None
 
161
        messages = messages[start:end]
 
162
 
 
163
        response = {
 
164
            'messages': messages,
 
165
            'next_memo': next_memo,
 
166
            'previous_memo': previous_memo
 
167
            }
 
168
        return response
 
169
 
 
170
 
 
171
class ForkedFakeService:
 
172
    """A Grackle service fake, as a ContextManager."""
 
173
 
 
174
    def __init__(self, port, messages=None, write_logs=False):
 
175
        """Constructor.
 
176
 
 
177
        :param port: The tcp port to use.
 
178
        :param messages: A dict of lists of dicts representing messages.  The
 
179
            outer dict represents the archive, the list represents the list of
 
180
            messages for that archive.
 
181
        :param write_logs: If true, log messages will be written to stdout.
 
182
        """
22
183
        self.pid = None
23
 
        self.args = args
 
184
        self.port = port
 
185
        if messages is None:
 
186
            self.messages = {}
 
187
        else:
 
188
            self.messages = messages
 
189
        self.read_end, self.write_end = os.pipe()
 
190
        self.write_logs = write_logs
 
191
 
 
192
    @staticmethod
 
193
    def from_client(client, messages=None):
 
194
        """Instantiate a ForkedFakeService from the client.
 
195
 
 
196
        :param port: The client to provide service for.
 
197
        :param messages: A dict of lists of dicts representing messages.  The
 
198
            outer dict represents the archive, the list represents the list of
 
199
            messages for that archive.
 
200
        """
 
201
        return ForkedFakeService(client.port, messages)
 
202
 
 
203
    def is_ready(self):
 
204
        """Tell the parent process that the server is ready for writes."""
 
205
        os.write(self.write_end, 'asdf')
24
206
 
25
207
    def __enter__(self):
 
208
        """Run the service.
 
209
 
 
210
        Fork and start a server in the child.  Return when the server is ready
 
211
        for use."""
26
212
        pid = os.fork()
27
 
        if pid != 0:
28
 
            self.pid = pid
29
 
            return
30
 
        self.func_or_method(*self.args)
 
213
        if pid == 0:
 
214
            self.start_server()
 
215
        self.pid = pid
 
216
        os.read(self.read_end, 1)
 
217
        return
31
218
 
 
219
    def start_server(self):
 
220
        """Start the HTTP server."""
 
221
        service = HTTPServer(('', self.port), FakeGrackleRequestHandler)
 
222
        service.store = GrackleStore(self.messages)
 
223
        for archive_id, messages in service.store.messages.iteritems():
 
224
            for message in messages:
 
225
                message.setdefault('headers', {})
 
226
        self.is_ready()
 
227
        if self.write_logs:
 
228
            logging.basicConfig(
 
229
                stream=sys.stderr, level=logging.INFO)
 
230
        service.serve_forever()
32
231
 
33
232
    def __exit__(self, exc_type, exc_val, traceback):
34
233
        os.kill(self.pid, SIGKILL)
35
234
 
36
235
 
 
236
SUPPORTED_DISPLAY_TYPES = set(['all', 'text-only', 'headers-only'])
 
237
 
 
238
 
 
239
SUPPORTED_ORDERS = set(
 
240
    ['date', 'author', 'subject', 'thread_newest', 'thread_oldest',
 
241
     'thread_subject'])
 
242
 
 
243
 
37
244
class FakeGrackleRequestHandler(BaseHTTPRequestHandler):
38
 
 
39
 
    def do_PUT(self):
 
245
    """A request handler that forwards to server.store."""
 
246
 
 
247
    def __init__(self, *args, **kwargs):
 
248
        """Constructor.  Sets up logging."""
 
249
        self.logger = logging.getLogger('http')
 
250
        BaseHTTPRequestHandler.__init__(self, *args, **kwargs)
 
251
 
 
252
    def do_POST(self):
 
253
        """Create a message on POST."""
40
254
        message = self.rfile.read(int(self.headers['content-length']))
41
255
        if message == 'This is a message':
42
256
            self.send_response(httplib.CREATED)
45
259
        else:
46
260
            self.send_error(httplib.BAD_REQUEST)
47
261
 
48
 
 
49
 
def run_service(port):
50
 
    service = HTTPServer(('', port), FakeGrackleRequestHandler)
51
 
    service.serve_forever()
52
 
 
 
262
    def do_GET(self):
 
263
        """Retrieve a list of messages on GET."""
 
264
        scheme, netloc, path, params, query_string, fragments = (
 
265
            urlparse(self.path))
 
266
        parts = path.split('/')
 
267
        if parts[1] == 'archive':
 
268
            try:
 
269
                response = self.server.store.get_messages(
 
270
                    parts[2], query_string)
 
271
                self.send_response(httplib.OK)
 
272
                self.end_headers()
 
273
                self.wfile.write(simplejson.dumps(response))
 
274
            except UnsupportedOrder:
 
275
                self.send_response(
 
276
                    httplib.BAD_REQUEST, UnsupportedOrder.__doc__)
 
277
                return
 
278
            except UnsupportedDisplayType:
 
279
                self.send_response(
 
280
                    httplib.BAD_REQUEST, UnsupportedDisplayType.__doc__)
 
281
                return
 
282
 
 
283
    def log_message(self, format, *args):
 
284
        """Override log_message to use standard Python logging."""
 
285
        message = "%s - - [%s] %s\n" % (
 
286
            self.address_string(), self.log_date_time_string(), format % args)
 
287
        self.logger.info(message)
53
288
 
54
289
 
55
290
class TestPutMessage(TestCase):
56
291
 
57
292
    def test_put_message(self):
58
 
        client = GrackleClient('localhost', 8435)
59
 
        with Forked(run_service, client.port):
 
293
        client = GrackleClient('localhost', 8436)
 
294
        with ForkedFakeService.from_client(client):
60
295
            client.put_message('arch1', 'asdf', StringIO('This is a message'))
61
296
            with ExpectedException(Exception, 'wtf'):
62
297
                client.put_message('arch1', 'asdf',
63
298
                    StringIO('This is not a message'))
 
299
 
 
300
 
 
301
class TestGetMessages(TestCase):
 
302
 
 
303
    def assertIDOrder(self, ids, messages):
 
304
        self.assertEqual(ids, [m['message_id'] for m in messages])
 
305
 
 
306
    def assertMessageIDs(self, ids, messages):
 
307
        self.assertIDOrder(
 
308
            sorted(ids), sorted(messages, key=lambda m: m['message_id']))
 
309
 
 
310
    def test_get_messages(self):
 
311
        client = GrackleClient('localhost', 8435)
 
312
        archive = {
 
313
            'baz': [make_message('foo'), make_message('bar')]}
 
314
        with ForkedFakeService.from_client(client, archive):
 
315
            response = client.get_messages('baz')
 
316
        self.assertEqual(['bar', 'foo'], sorted(m['message_id'] for m in
 
317
            response['messages']))
 
318
        self.assertIs(None, response['next_memo'])
 
319
        self.assertIs(None, response['previous_memo'])
 
320
 
 
321
    def test_get_messages_by_id(self):
 
322
        client = GrackleClient('localhost', 8437)
 
323
        archive = {
 
324
            'baz': [make_message('foo'), make_message('bar')]}
 
325
        with ForkedFakeService.from_client(client, archive):
 
326
            response = client.get_messages('baz', message_ids=['foo'])
 
327
        message, = response['messages']
 
328
        self.assertEqual('foo', message['message_id'])
 
329
 
 
330
    def test_get_messages_batching(self):
 
331
        client = GrackleClient('localhost', 8438)
 
332
        archive = {'baz': [make_message('foo'), make_message('bar')]}
 
333
        with ForkedFakeService.from_client(client, archive):
 
334
            response = client.get_messages('baz', limit=1)
 
335
            self.assertEqual(1, len(response['messages']))
 
336
            messages = response['messages']
 
337
            response = client.get_messages(
 
338
                'baz', limit=1, memo=response['next_memo'])
 
339
            self.assertEqual(1, len(response['messages']))
 
340
            messages.extend(response['messages'])
 
341
            self.assertMessageIDs(['foo', 'bar'], messages)
 
342
 
 
343
    def get_messages_member_order_test(self, key):
 
344
        client = GrackleClient('localhost', 8439)
 
345
        archive = {
 
346
            'baz': [
 
347
                make_message('foo', headers={key: '2011-03-25'}),
 
348
                make_message('bar', headers={key: '2011-03-24'}),
 
349
             ]}
 
350
        with ForkedFakeService.from_client(client, archive):
 
351
            response = client.get_messages('baz')
 
352
            self.assertIDOrder(['foo', 'bar'], response['messages'])
 
353
            response = client.get_messages('baz', order=key)
 
354
            self.assertIDOrder(['bar', 'foo'], response['messages'])
 
355
 
 
356
    def test_get_messages_date_order(self):
 
357
        self.get_messages_member_order_test('date')
 
358
 
 
359
    def test_get_messages_author_order(self):
 
360
        self.get_messages_member_order_test('author')
 
361
 
 
362
    def test_get_messages_subject_order(self):
 
363
        self.get_messages_member_order_test('subject')
 
364
 
 
365
    def test_get_messages_thread_subject_order(self):
 
366
        archive = {
 
367
            'baz': [
 
368
                make_message('bar', headers={'subject': 'y'}),
 
369
                make_message('qux', headers={'subject': 'z'}),
 
370
                make_message('foo', headers={'subject': 'x',
 
371
                                             'in-reply-to': 'qux'}),
 
372
             ]}
 
373
        client = GrackleClient('localhost', 8439)
 
374
        with ForkedFakeService.from_client(client, archive):
 
375
            response = client.get_messages('baz')
 
376
            self.assertIDOrder(['bar', 'qux', 'foo'], response['messages'])
 
377
            response = client.get_messages('baz', order='subject')
 
378
            self.assertIDOrder(['foo', 'bar', 'qux'], response['messages'])
 
379
            response = client.get_messages('baz', order='thread_subject')
 
380
            self.assertIDOrder(['bar', 'qux', 'foo'], response['messages'])
 
381
 
 
382
    def test_get_messages_thread_oldest_order(self):
 
383
        client = GrackleClient('localhost', 8439)
 
384
        archive = {
 
385
            'baz': [
 
386
                make_message('bar', headers={'date': 'x'}),
 
387
                make_message('qux', headers={'date': 'z'}),
 
388
                make_message('foo', headers={'date': 'y',
 
389
                                             'in-reply-to': 'qux'}),
 
390
            ]}
 
391
        with ForkedFakeService.from_client(client, archive):
 
392
            response = client.get_messages('baz')
 
393
            self.assertIDOrder(['bar', 'qux', 'foo'], response['messages'])
 
394
            response = client.get_messages('baz', order='date')
 
395
            self.assertIDOrder(['bar', 'foo', 'qux'], response['messages'])
 
396
            response = client.get_messages('baz', order='thread_oldest')
 
397
            self.assertIDOrder(['bar', 'qux', 'foo'], response['messages'])
 
398
 
 
399
    def test_get_messages_thread_newest_order(self):
 
400
        client = GrackleClient('localhost', 8439)
 
401
        archive = {
 
402
            'baz': [
 
403
                make_message('bar', headers={'date': 'x'}),
 
404
                make_message('qux', headers={'date': 'w'}),
 
405
                make_message('foo', headers={'date': 'y',
 
406
                                             'in-reply-to': 'bar'}),
 
407
                make_message('baz', headers={'date': 'z',
 
408
                                             'in-reply-to': 'qux'}),
 
409
            ]}
 
410
        with ForkedFakeService.from_client(client, archive):
 
411
            response = client.get_messages('baz', order='date')
 
412
            self.assertIDOrder(
 
413
                ['qux', 'bar', 'foo', 'baz'], response['messages'])
 
414
            response = client.get_messages('baz', order='thread_newest')
 
415
            self.assertIDOrder(
 
416
                ['bar', 'foo', 'qux', 'baz'], response['messages'])
 
417
 
 
418
    def test_get_messages_unsupported_order(self):
 
419
        client = GrackleClient('localhost', 8439)
 
420
        archive = {
 
421
            'baz': [
 
422
                make_message('foo', headers={'date': '2011-03-25'}),
 
423
                make_message('foo', headers={'date': '2011-03-24'}),
 
424
            ]}
 
425
        with ForkedFakeService.from_client(client, archive):
 
426
            with ExpectedException(UnsupportedOrder, ''):
 
427
                client.get_messages('baz', order='nonsense')
 
428
 
 
429
    def test_get_messages_headers_no_headers(self):
 
430
        client = GrackleClient('localhost', 8440)
 
431
        archive = {'baz': [make_message('foo')]}
 
432
        with ForkedFakeService.from_client(client, archive):
 
433
            response = client.get_messages('baz', headers=[
 
434
                'Subject', 'Date', 'X-Launchpad-Message-Rationale'])
 
435
        first_message = response['messages'][0]
 
436
        self.assertEqual('foo', first_message['message_id'])
 
437
        self.assertEqual({}, first_message['headers'])
 
438
 
 
439
    def test_get_messages_headers_exclude_headers(self):
 
440
        client = GrackleClient('localhost', 8441)
 
441
        archive = {
 
442
            'baz': [make_message('foo', headers={'From': 'me'})]}
 
443
        with ForkedFakeService.from_client(client, archive):
 
444
            response = client.get_messages('baz', headers=[
 
445
                'Subject', 'Date', 'X-Launchpad-Message-Rationale'])
 
446
        first_message = response['messages'][0]
 
447
        self.assertEqual('foo', first_message['message_id'])
 
448
        self.assertEqual({}, first_message['headers'])
 
449
 
 
450
    def test_get_messages_headers_include_headers(self):
 
451
        client = GrackleClient('localhost', 8442)
 
452
        archive = {
 
453
            'baz': [
 
454
                make_message('foo', headers={'From': 'me', 'To': 'you'})]}
 
455
        with ForkedFakeService.from_client(client, archive):
 
456
            response = client.get_messages('baz', headers=[
 
457
                'From', 'To'])
 
458
        first_message = response['messages'][0]
 
459
        self.assertEqual('foo', first_message['message_id'])
 
460
        self.assertEqual({'From': 'me', 'To': 'you'}, first_message['headers'])
 
461
 
 
462
    def test_get_messages_max_body_length(self):
 
463
        client = GrackleClient('localhost', 8443)
 
464
        archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
 
465
        with ForkedFakeService.from_client(client, archive):
 
466
            response = client.get_messages('baz', max_body_length=3)
 
467
        first_message = response['messages'][0]
 
468
        self.assertEqual('abc', first_message['body'])
 
469
 
 
470
    def test_include_hidden(self):
 
471
        client = GrackleClient('localhost', 8444)
 
472
        archive = {
 
473
            'baz': [
 
474
                make_message('foo', hidden=True),
 
475
                make_message('bar', hidden=False),
 
476
            ]}
 
477
        with ForkedFakeService.from_client(client, archive):
 
478
            response = client.get_messages('baz', include_hidden=True)
 
479
            self.assertMessageIDs(['bar', 'foo'], response['messages'])
 
480
            response = client.get_messages('baz', include_hidden=False)
 
481
            self.assertMessageIDs(['bar'], response['messages'])
 
482
 
 
483
    def test_display_type_unknown_value(self):
 
484
        client = GrackleClient('localhost', 8445)
 
485
        archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
 
486
        with ForkedFakeService.from_client(client, archive):
 
487
            with ExpectedException(UnsupportedDisplayType, ''):
 
488
                client.get_messages('baz', display_type='unknown')
 
489
 
 
490
    def test_display_type_headers_only(self):
 
491
        client = GrackleClient('localhost', 8446)
 
492
        archive = {
 
493
            'baz': [
 
494
                make_message('foo', body=u'abcdefghi',
 
495
                             headers={'From': 'me', 'To': 'you'})]}
 
496
        with ForkedFakeService.from_client(client, archive):
 
497
            response = client.get_messages('baz', display_type='headers-only')
 
498
        first_message = response['messages'][0]
 
499
        self.assertEqual('foo', first_message['message_id'])
 
500
        self.assertEqual(
 
501
            {'From': 'me', 'Message-Id': 'foo', 'To': 'you'},
 
502
            first_message['headers'])
 
503
        self.assertNotIn('body', first_message)
 
504
 
 
505
    def test_display_type_text_only(self):
 
506
        client = GrackleClient('localhost', 8446)
 
507
        archive = {
 
508
            'baz': [
 
509
                make_mime_message(
 
510
                    'foo', 'abcdefghi',
 
511
                    headers={'From': 'me', 'To': 'you'},
 
512
                    attachment_type='text/x-diff')]}
 
513
        with ForkedFakeService.from_client(client, archive):
 
514
            response = client.get_messages('baz', display_type='text-only')
 
515
        first_message = response['messages'][0]
 
516
        self.assertEqual('foo', first_message['message_id'])
 
517
        self.assertEqual('me', first_message['headers']['From'])
 
518
        self.assertEqual('you', first_message['headers']['To'])
 
519
        self.assertEqual('abcdefghi', first_message['body'])
 
520
 
 
521
    def test_display_type_all(self):
 
522
        client = GrackleClient('localhost', 8447)
 
523
        archive = {
 
524
            'baz': [
 
525
                make_mime_message(
 
526
                    'foo', 'abcdefghi',
 
527
                    headers={'From': 'me', 'To': 'you'},
 
528
                    attachment_type='text/x-diff')]}
 
529
        with ForkedFakeService.from_client(client, archive):
 
530
            response = client.get_messages('baz', display_type='all')
 
531
        first_message = response['messages'][0]
 
532
        self.assertEqual('foo', first_message['message_id'])
 
533
        self.assertEqual('me', first_message['headers']['From'])
 
534
        self.assertEqual('you', first_message['headers']['To'])
 
535
        self.assertEqual(
 
536
            'abcdefghi\n\nattactment data.', first_message['body'])