~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:
1
 
__metaclass__ = type
2
 
 
 
1
from BaseHTTPServer import (
 
2
    HTTPServer,
 
3
    BaseHTTPRequestHandler,
 
4
    )
3
5
from email.message import Message
4
6
from email.mime.multipart import MIMEMultipart
5
7
from email.mime.text import MIMEText
 
8
import httplib
 
9
import logging
 
10
import os
 
11
from signal import SIGKILL
 
12
import simplejson
6
13
from StringIO import StringIO
 
14
import sys
7
15
from unittest import TestCase
 
16
from urlparse import urlparse
 
17
from urlparse import parse_qs
8
18
 
9
19
from testtools import ExpectedException
10
20
 
11
 
from grackle.client import GrackleClient
12
 
from grackle.error import (
13
 
    ArchiveIdExists,
14
 
    UnparsableDateRange,
 
21
from grackle.client import (
 
22
    GrackleClient,
15
23
    UnsupportedDisplayType,
16
24
    UnsupportedOrder,
17
25
    )
18
 
from grackle.service import ForkedFakeService
19
 
from grackle.store import make_json_message
20
26
 
21
27
 
22
28
def make_message(message_id, body='body', headers=None, hidden=False):
23
29
    if headers is None:
24
30
        headers = {}
25
 
    message_headers = {
26
 
        'Message-Id': message_id,
27
 
        'date': '2005-01-01',
28
 
        'subject': 'subject',
29
 
        'from': 'author',
30
 
        'replies': '',
 
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,
31
43
        }
32
 
    message_headers.update(headers.items())
33
 
    message = Message()
34
 
    message.set_payload(body)
35
 
    for key, value in message_headers.items():
36
 
        message[key] = value
37
 
    return make_json_message(message_id, message.as_string(), hidden)
 
44
    return message
38
45
 
39
46
 
40
47
def make_mime_message(message_id, body='body', headers=None, hidden=False,
41
48
                      attachment_type=None):
42
 
    parts = MIMEMultipart()
43
 
    parts.attach(MIMEText(body))
 
49
    message = MIMEMultipart()
 
50
    message.attach(MIMEText(body))
44
51
    if attachment_type is not None:
45
52
        attachment = Message()
46
53
        attachment.set_payload('attactment data.')
47
54
        attachment['Content-Type'] = attachment_type
48
55
        attachment['Content-Disposition'] = 'attachment; filename="file.ext"'
49
 
        parts.attach(attachment)
50
 
    return make_message(message_id, parts.as_string(), headers, hidden)
51
 
 
52
 
 
53
 
class TestPutArchive(TestCase):
54
 
 
55
 
    def test_put_archive(self):
56
 
        client = GrackleClient('localhost', 8410)
57
 
        message_archives = {}
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']))
62
 
 
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')
 
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
        """
 
183
        self.pid = None
 
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')
 
206
 
 
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."""
 
212
        pid = os.fork()
 
213
        if pid == 0:
 
214
            self.start_server()
 
215
        self.pid = pid
 
216
        os.read(self.read_end, 1)
 
217
        return
 
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()
 
231
 
 
232
    def __exit__(self, exc_type, exc_val, traceback):
 
233
        os.kill(self.pid, SIGKILL)
 
234
 
 
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
 
 
244
class FakeGrackleRequestHandler(BaseHTTPRequestHandler):
 
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."""
 
254
        message = self.rfile.read(int(self.headers['content-length']))
 
255
        if message == 'This is a message':
 
256
            self.send_response(httplib.CREATED)
 
257
            self.end_headers()
 
258
            self.wfile.close()
 
259
        else:
 
260
            self.send_error(httplib.BAD_REQUEST)
 
261
 
 
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)
69
288
 
70
289
 
71
290
class TestPutMessage(TestCase):
72
291
 
73
292
    def test_put_message(self):
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'])
82
 
 
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):
 
293
        client = GrackleClient('localhost', 8436)
 
294
        with ForkedFakeService.from_client(client):
 
295
            client.put_message('arch1', 'asdf', StringIO('This is a message'))
87
296
            with ExpectedException(Exception, 'wtf'):
88
 
                client.put_message('no-archive', 'id1', StringIO('message'))
 
297
                client.put_message('arch1', 'asdf',
 
298
                    StringIO('This is not a message'))
89
299
 
90
300
 
91
301
class TestGetMessages(TestCase):
98
308
            sorted(ids), sorted(messages, key=lambda m: m['message_id']))
99
309
 
100
310
    def test_get_messages(self):
101
 
        client = GrackleClient('localhost', 8430)
 
311
        client = GrackleClient('localhost', 8435)
102
312
        archive = {
103
313
            'baz': [make_message('foo'), make_message('bar')]}
104
314
        with ForkedFakeService.from_client(client, archive):
132
342
 
133
343
    def get_messages_member_order_test(self, key):
134
344
        client = GrackleClient('localhost', 8439)
135
 
        if key == 'author':
136
 
            header_name = 'from'
137
 
        else:
138
 
            header_name = key
139
345
        archive = {
140
346
            'baz': [
141
 
                make_message('foo', headers={header_name: '2011-03-25'}),
142
 
                make_message('bar', headers={header_name: '2011-03-24'}),
 
347
                make_message('foo', headers={key: '2011-03-25'}),
 
348
                make_message('bar', headers={key: '2011-03-24'}),
143
349
             ]}
144
350
        with ForkedFakeService.from_client(client, archive):
145
351
            response = client.get_messages('baz')
292
498
        first_message = response['messages'][0]
293
499
        self.assertEqual('foo', first_message['message_id'])
294
500
        self.assertEqual(
295
 
            archive['baz'][0]['headers'], first_message['headers'])
 
501
            {'From': 'me', 'Message-Id': 'foo', 'To': 'you'},
 
502
            first_message['headers'])
296
503
        self.assertNotIn('body', first_message)
297
504
 
298
505
    def test_display_type_text_only(self):
309
516
        self.assertEqual('foo', first_message['message_id'])
310
517
        self.assertEqual('me', first_message['headers']['From'])
311
518
        self.assertEqual('you', first_message['headers']['To'])
312
 
        self.assertEqual(archive['baz'][0]['body'], first_message['body'])
 
519
        self.assertEqual('abcdefghi', first_message['body'])
313
520
 
314
521
    def test_display_type_all(self):
315
522
        client = GrackleClient('localhost', 8447)
325
532
        self.assertEqual('foo', first_message['message_id'])
326
533
        self.assertEqual('me', first_message['headers']['From'])
327
534
        self.assertEqual('you', first_message['headers']['To'])
328
 
        self.assertEqual(archive['baz'][0]['body'], first_message['body'])
329
 
 
330
 
    def test_date_range(self):
331
 
        client = GrackleClient('localhost', 8448)
332
 
        archive = {
333
 
            'baz': [
334
 
                make_mime_message(
335
 
                    'foo', 'abcdefghi', headers={'date': '2011-12-31'}),
336
 
                make_mime_message(
337
 
                    'bar', 'abcdefghi', headers={'date': '2012-01-01'}),
338
 
                make_mime_message(
339
 
                    'qux', 'abcdefghi', headers={'date': '2012-01-15'}),
340
 
                make_mime_message(
341
 
                    'naf', 'abcdefghi', headers={'date': '2012-01-31'}),
342
 
                make_mime_message(
343
 
                    'doh', 'abcdefghi', headers={'date': '2012-02-01'}),
344
 
                    ]}
345
 
        with ForkedFakeService.from_client(client, archive):
346
 
            response = client.get_messages(
347
 
                'baz', date_range='2012-01-01..2012-01-31')
348
 
        ids = sorted(m['message_id'] for m in response['messages'])
349
 
        self.assertEqual(['bar', 'naf', 'qux'], ids)
350
 
 
351
 
    def test_date_range_unparsabledaterange(self):
352
 
        client = GrackleClient('localhost', 8449)
353
 
        archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
354
 
        with ForkedFakeService.from_client(client, archive):
355
 
            with ExpectedException(UnparsableDateRange, ''):
356
 
                client.get_messages('baz', date_range='2012-01-01')
357
 
 
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..')
364
 
 
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')
371
 
 
372
 
 
373
 
class TestHideMessages(TestCase):
374
 
 
375
 
    def test_hide_message_true(self):
376
 
        client = GrackleClient('localhost', 8470)
377
 
        archive = {
378
 
            'baz': [
379
 
                make_message('foo', hidden=False),
380
 
            ]}
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'])
385
 
 
386
 
    def test_hide_message_false(self):
387
 
        client = GrackleClient('localhost', 8470)
388
 
        archive = {
389
 
            'baz': [
390
 
                make_message('foo', hidden=True),
391
 
            ]}
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'])
 
535
        self.assertEqual(
 
536
            'abcdefghi\n\nattactment data.', first_message['body'])