~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 03:22:02 UTC
  • mto: This revision was merged to the branch mainline in revision 37.
  • Revision ID: curtis.hovey@canonical.com-20120131032202-b4r9msw4107zvkig
Reanme make_json_message => make_message.

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