~didrocks/unity/altf10

« back to all changes in this revision

Viewing changes to grackle/tests/test_client.py

  • Committer: Curtis Hovey
  • Date: 2012-03-17 22:45:15 UTC
  • Revision ID: curtis.hovey@canonical.com-20120317224515-r2n23tqc8cx7cul4
Only store the unique information needed by grackle.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
from BaseHTTPServer import (
2
 
    HTTPServer,
3
 
    BaseHTTPRequestHandler,
4
 
    )
 
1
__metaclass__ = type
 
2
 
5
3
from email.message import Message
6
4
from email.mime.multipart import MIMEMultipart
7
5
from email.mime.text import MIMEText
8
 
import httplib
9
 
import logging
10
 
import os
11
 
from signal import SIGKILL
12
 
import simplejson
13
6
from StringIO import StringIO
14
 
import sys
15
7
from unittest import TestCase
16
 
from urlparse import urlparse
17
 
from urlparse import parse_qs
18
8
 
19
9
from testtools import ExpectedException
20
10
 
21
 
from grackle.client import (
22
 
    GrackleClient,
 
11
from grackle.client import GrackleClient
 
12
from grackle.error import (
 
13
    ArchiveIdExists,
 
14
    UnparsableDateRange,
23
15
    UnsupportedDisplayType,
24
16
    UnsupportedOrder,
25
17
    )
 
18
from grackle.service import ForkedFakeService
 
19
from grackle.store import make_json_message
26
20
 
27
21
 
28
22
def make_message(message_id, body='body', headers=None, hidden=False):
29
23
    if headers is None:
30
24
        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,
 
25
    message_headers = {
 
26
        'Message-Id': message_id,
 
27
        'date': '2005-01-01',
 
28
        'subject': 'subject',
 
29
        'from': 'author',
 
30
        'replies': '',
43
31
        }
44
 
    return message
 
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)
45
38
 
46
39
 
47
40
def make_mime_message(message_id, body='body', headers=None, hidden=False,
48
41
                      attachment_type=None):
49
 
    message = MIMEMultipart()
50
 
    message.attach(MIMEText(body))
 
42
    parts = MIMEMultipart()
 
43
    parts.attach(MIMEText(body))
51
44
    if attachment_type is not None:
52
45
        attachment = Message()
53
46
        attachment.set_payload('attactment data.')
54
47
        attachment['Content-Type'] = attachment_type
55
48
        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
 
        """
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)
 
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')
288
69
 
289
70
 
290
71
class TestPutMessage(TestCase):
291
72
 
292
73
    def test_put_message(self):
293
 
        client = GrackleClient('localhost', 8436)
294
 
        with ForkedFakeService.from_client(client):
295
 
            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'])
 
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):
296
87
            with ExpectedException(Exception, 'wtf'):
297
 
                client.put_message('arch1', 'asdf',
298
 
                    StringIO('This is not a message'))
 
88
                client.put_message('no-archive', 'id1', StringIO('message'))
299
89
 
300
90
 
301
91
class TestGetMessages(TestCase):
308
98
            sorted(ids), sorted(messages, key=lambda m: m['message_id']))
309
99
 
310
100
    def test_get_messages(self):
311
 
        client = GrackleClient('localhost', 8435)
 
101
        client = GrackleClient('localhost', 8430)
312
102
        archive = {
313
103
            'baz': [make_message('foo'), make_message('bar')]}
314
104
        with ForkedFakeService.from_client(client, archive):
342
132
 
343
133
    def get_messages_member_order_test(self, key):
344
134
        client = GrackleClient('localhost', 8439)
 
135
        if key == 'author':
 
136
            header_name = 'from'
 
137
        else:
 
138
            header_name = key
345
139
        archive = {
346
140
            'baz': [
347
 
                make_message('foo', headers={key: '2011-03-25'}),
348
 
                make_message('bar', headers={key: '2011-03-24'}),
 
141
                make_message('foo', headers={header_name: '2011-03-25'}),
 
142
                make_message('bar', headers={header_name: '2011-03-24'}),
349
143
             ]}
350
144
        with ForkedFakeService.from_client(client, archive):
351
145
            response = client.get_messages('baz')
498
292
        first_message = response['messages'][0]
499
293
        self.assertEqual('foo', first_message['message_id'])
500
294
        self.assertEqual(
501
 
            {'From': 'me', 'Message-Id': 'foo', 'To': 'you'},
502
 
            first_message['headers'])
 
295
            archive['baz'][0]['headers'], first_message['headers'])
503
296
        self.assertNotIn('body', first_message)
504
297
 
505
298
    def test_display_type_text_only(self):
516
309
        self.assertEqual('foo', first_message['message_id'])
517
310
        self.assertEqual('me', first_message['headers']['From'])
518
311
        self.assertEqual('you', first_message['headers']['To'])
519
 
        self.assertEqual('abcdefghi', first_message['body'])
 
312
        self.assertEqual(archive['baz'][0]['body'], first_message['body'])
520
313
 
521
314
    def test_display_type_all(self):
522
315
        client = GrackleClient('localhost', 8447)
532
325
        self.assertEqual('foo', first_message['message_id'])
533
326
        self.assertEqual('me', first_message['headers']['From'])
534
327
        self.assertEqual('you', first_message['headers']['To'])
535
 
        self.assertEqual(
536
 
            'abcdefghi\n\nattactment data.', first_message['body'])
 
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'])