~didrocks/unity/altf10

« back to all changes in this revision

Viewing changes to grackle/tests/test_client.py

  • Committer: Curtis Hovey
  • Date: 2012-02-14 22:49:46 UTC
  • Revision ID: curtis.hovey@canonical.com-20120214224946-rx83gm3er2pho566
Raise UnparsableDateRange when the date cannot be parsed.

Show diffs side-by-side

added added

removed removed

Lines of Context:
14
14
import sys
15
15
from unittest import TestCase
16
16
from urlparse import urlparse
 
17
from urlparse import parse_qs
17
18
 
18
19
from testtools import ExpectedException
19
20
 
23
24
    UnsupportedDisplayType,
24
25
    UnsupportedOrder,
25
26
    )
26
 
from grackle.store import (
27
 
    make_json_message,
28
 
    MemoryStore,
29
 
    )
30
27
 
31
28
 
32
29
def make_message(message_id, body='body', headers=None, hidden=False):
33
30
    if headers is None:
34
31
        headers = {}
35
 
    message_headers = {
36
 
        'Message-Id': message_id,
37
 
        'date': '2005-01-01',
38
 
        'subject': 'subject',
39
 
        'from': 'author',
40
 
        'replies': '',
 
32
    headers['Message-Id'] = message_id
 
33
    message = {
 
34
        'message_id': message_id,
 
35
        'headers': headers,
 
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'),
 
40
        'hidden': hidden,
 
41
        'attachments': [],
 
42
        'replies': headers.get('in-reply-to', None),
 
43
        'body': body,
41
44
        }
42
 
    message_headers.update(headers.items())
43
 
    message = Message()
44
 
    message.set_payload(body)
45
 
    for key, value in message_headers.items():
46
 
        message[key] = value
47
 
    return make_json_message(message_id, message.as_string(), hidden)
 
45
    return message
48
46
 
49
47
 
50
48
def make_mime_message(message_id, body='body', headers=None, hidden=False,
51
49
                      attachment_type=None):
52
 
    parts = MIMEMultipart()
53
 
    parts.attach(MIMEText(body))
 
50
    message = MIMEMultipart()
 
51
    message.attach(MIMEText(body))
54
52
    if attachment_type is not None:
55
53
        attachment = Message()
56
54
        attachment.set_payload('attactment data.')
57
55
        attachment['Content-Type'] = attachment_type
58
56
        attachment['Content-Disposition'] = 'attachment; filename="file.ext"'
59
 
        parts.attach(attachment)
60
 
    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)
 
59
 
 
60
 
 
61
def threaded_messages(messages):
 
62
    threads = {}
 
63
    count = 0
 
64
    pending = []
 
65
    for message in messages:
 
66
        if message.get('replies') is None:
 
67
            threads[message['message_id']] = [message]
 
68
            count += 1
 
69
        else:
 
70
            pending.append(message)
 
71
    for message in pending:
 
72
        threads[message['replies']].append(message)
 
73
    return threads.values()
 
74
 
 
75
 
 
76
class GrackleStore:
 
77
    """A memory-backed message store."""
 
78
 
 
79
    def __init__(self, messages):
 
80
        """Constructor."""
 
81
        self.messages = messages
 
82
 
 
83
    @staticmethod
 
84
    def is_multipart(message):
 
85
        return isinstance(message['body'], list)
 
86
 
 
87
    def get_messages(self, archive_id, query_string):
 
88
        """Return matching messages.
 
89
 
 
90
        :param archive_id: The archive to retrieve from.
 
91
        :param query_string: Contains 'parameters', which is a JSON-format
 
92
            string describing parameters.
 
93
        """
 
94
        query = parse_qs(query_string)
 
95
        parameters = simplejson.loads(query['parameters'][0])
 
96
        order = parameters.get('order')
 
97
        messages = self.messages[archive_id]
 
98
        if order is not None:
 
99
            if order not in SUPPORTED_ORDERS:
 
100
                raise UnsupportedOrder
 
101
            elif order.startswith('thread_'):
 
102
                threaded = threaded_messages(messages)
 
103
                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)
 
112
            else:
 
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:
 
118
            try:
 
119
                start_date, end_date = parameters['date_range'].split('..')
 
120
            except ValueError:
 
121
                raise UnparsableDateRange
 
122
        new_messages = []
 
123
        for message in messages:
 
124
            if (not parameters['include_hidden'] and message['hidden']):
 
125
                continue
 
126
            if ('message_ids' in parameters
 
127
                and message['message_id'] not in parameters['message_ids']):
 
128
                continue
 
129
            if ('date_range' in parameters
 
130
                and (message['date'] < start_date
 
131
                     or message['date'] > end_date)):
 
132
                continue
 
133
            message = dict(message)
 
134
            if 'headers' in parameters:
 
135
                headers = dict(
 
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':
 
140
                del message['body']
 
141
            elif display_type == 'text-only' and self.is_multipart(message):
 
142
                text_parts = [
 
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))
 
158
        if memo is None:
 
159
            start = 0
 
160
        else:
 
161
            start = message_id_indices[memo.encode('rot13')]
 
162
        if start > 0:
 
163
            previous_memo = messages[start - 1]['message_id'].encode('rot13')
 
164
        else:
 
165
            previous_memo = None
 
166
        end = min(start + limit, len(messages))
 
167
        if end < len(messages):
 
168
            next_memo = messages[end]['message_id'].encode('rot13')
 
169
        else:
 
170
            next_memo = None
 
171
        messages = messages[start:end]
 
172
 
 
173
        response = {
 
174
            'messages': messages,
 
175
            'next_memo': next_memo,
 
176
            'previous_memo': previous_memo
 
177
            }
 
178
        return response
61
179
 
62
180
 
63
181
class ForkedFakeService:
64
182
    """A Grackle service fake, as a ContextManager."""
65
183
 
66
 
    def __init__(self, port, message_archives=None, write_logs=False):
 
184
    def __init__(self, port, messages=None, write_logs=False):
67
185
        """Constructor.
68
186
 
69
187
        :param port: The tcp port to use.
70
 
        :param message_archives: A dict of lists of dicts representing
71
 
            archives of messages. The outer dict represents the archive,
72
 
            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.
73
191
        :param write_logs: If true, log messages will be written to stdout.
74
192
        """
75
193
        self.pid = None
76
194
        self.port = port
77
 
        if message_archives is None:
78
 
            self.message_archives = {}
 
195
        if messages is None:
 
196
            self.messages = {}
79
197
        else:
80
 
            self.message_archives = message_archives
 
198
            self.messages = messages
81
199
        self.read_end, self.write_end = os.pipe()
82
200
        self.write_logs = write_logs
83
201
 
84
202
    @staticmethod
85
 
    def from_client(client, message_archives=None):
 
203
    def from_client(client, messages=None):
86
204
        """Instantiate a ForkedFakeService from the client.
87
205
 
88
206
        :param port: The client to provide service for.
89
 
        :param message_archives: A dict of lists of dicts representing
90
 
            archives of messages. The outer dict represents the archive,
91
 
            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.
92
210
        """
93
 
        return ForkedFakeService(client.port, message_archives)
 
211
        return ForkedFakeService(client.port, messages)
94
212
 
95
213
    def is_ready(self):
96
214
        """Tell the parent process that the server is ready for writes."""
111
229
    def start_server(self):
112
230
        """Start the HTTP server."""
113
231
        service = HTTPServer(('', self.port), FakeGrackleRequestHandler)
114
 
        service.store = MemoryStore(self.message_archives)
115
 
        for archive_id, messages in service.store.message_archives.iteritems():
 
232
        service.store = GrackleStore(self.messages)
 
233
        for archive_id, messages in service.store.messages.iteritems():
116
234
            for message in messages:
117
235
                message.setdefault('headers', {})
118
236
        self.is_ready()
125
243
        os.kill(self.pid, SIGKILL)
126
244
 
127
245
 
 
246
SUPPORTED_DISPLAY_TYPES = set(['all', 'text-only', 'headers-only'])
 
247
 
 
248
 
 
249
SUPPORTED_ORDERS = set(
 
250
    ['date', 'author', 'subject', 'thread_newest', 'thread_oldest',
 
251
     'thread_subject'])
 
252
 
 
253
 
128
254
class FakeGrackleRequestHandler(BaseHTTPRequestHandler):
129
255
    """A request handler that forwards to server.store."""
130
256
 
135
261
 
136
262
    def do_POST(self):
137
263
        """Create a message on POST."""
138
 
        scheme, netloc, path, params, query_string, fragments = (
139
 
            urlparse(self.path))
140
 
        parts = path.split('/')
141
 
        if parts[1] != 'archive':
142
 
            # This is an unknonwn operation?
143
 
            return
144
 
        if 'content-length' in self.headers:
145
 
            operation = 'put_message'
 
264
        message = self.rfile.read(int(self.headers['content-length']))
 
265
        if message == 'This is a message':
 
266
            self.send_response(httplib.CREATED)
 
267
            self.end_headers()
 
268
            self.wfile.close()
146
269
        else:
147
 
            operation = 'hide_message'
148
 
        if operation == 'put_message':
149
 
            message = self.rfile.read(int(self.headers['content-length']))
150
 
            try:
151
 
                # This expected path is /archive/archive_id/message_id.
152
 
                self.server.store.put_message(parts[2], parts[3], message)
153
 
                self.send_response(httplib.CREATED)
154
 
                self.end_headers()
155
 
                self.wfile.close()
156
 
            except:
157
 
                self.send_error(httplib.BAD_REQUEST)
158
 
        elif operation == 'hide_message':
159
 
            try:
160
 
                # This expected path is /archive/archive_id/message_id.
161
 
                response = self.server.store.hide_message(
162
 
                    parts[2], parts[3], query_string)
163
 
                self.send_response(httplib.OK)
164
 
                self.end_headers()
165
 
                self.wfile.write(simplejson.dumps(response))
166
 
            except:
167
 
                self.send_error(httplib.BAD_REQUEST)
 
270
            self.send_error(httplib.BAD_REQUEST)
168
271
 
169
272
    def do_GET(self):
170
273
        """Retrieve a list of messages on GET."""
193
296
class TestPutMessage(TestCase):
194
297
 
195
298
    def test_put_message(self):
196
 
        client = GrackleClient('localhost', 8420)
197
 
        message_archives = {'arch1': []}
198
 
        with ForkedFakeService.from_client(client, message_archives):
199
 
            client.put_message('arch1', 'id1', StringIO('This is a message'))
200
 
            response = client.get_messages('arch1')
201
 
        self.assertEqual(1, len(response['messages']))
202
 
        message = response['messages'][0]
203
 
        self.assertEqual('id1', message['message_id'])
204
 
 
205
 
    def test_put_message_without_archive(self):
206
 
        client = GrackleClient('localhost', 8421)
207
 
        message_archives = {'arch1': []}
208
 
        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'))
209
302
            with ExpectedException(Exception, 'wtf'):
210
 
                client.put_message('no-archive', 'id1', StringIO('message'))
 
303
                client.put_message('arch1', 'asdf',
 
304
                    StringIO('This is not a message'))
211
305
 
212
306
 
213
307
class TestGetMessages(TestCase):
220
314
            sorted(ids), sorted(messages, key=lambda m: m['message_id']))
221
315
 
222
316
    def test_get_messages(self):
223
 
        client = GrackleClient('localhost', 8430)
 
317
        client = GrackleClient('localhost', 8435)
224
318
        archive = {
225
319
            'baz': [make_message('foo'), make_message('bar')]}
226
320
        with ForkedFakeService.from_client(client, archive):
254
348
 
255
349
    def get_messages_member_order_test(self, key):
256
350
        client = GrackleClient('localhost', 8439)
257
 
        if key == 'author':
258
 
            header_name = 'from'
259
 
        else:
260
 
            header_name = key
261
351
        archive = {
262
352
            'baz': [
263
 
                make_message('foo', headers={header_name: '2011-03-25'}),
264
 
                make_message('bar', headers={header_name: '2011-03-24'}),
 
353
                make_message('foo', headers={key: '2011-03-25'}),
 
354
                make_message('bar', headers={key: '2011-03-24'}),
265
355
             ]}
266
356
        with ForkedFakeService.from_client(client, archive):
267
357
            response = client.get_messages('baz')
414
504
        first_message = response['messages'][0]
415
505
        self.assertEqual('foo', first_message['message_id'])
416
506
        self.assertEqual(
417
 
            archive['baz'][0]['headers'], first_message['headers'])
 
507
            {'From': 'me', 'Message-Id': 'foo', 'To': 'you'},
 
508
            first_message['headers'])
418
509
        self.assertNotIn('body', first_message)
419
510
 
420
511
    def test_display_type_text_only(self):
431
522
        self.assertEqual('foo', first_message['message_id'])
432
523
        self.assertEqual('me', first_message['headers']['From'])
433
524
        self.assertEqual('you', first_message['headers']['To'])
434
 
        self.assertEqual(archive['baz'][0]['body'], first_message['body'])
 
525
        self.assertEqual('abcdefghi', first_message['body'])
435
526
 
436
527
    def test_display_type_all(self):
437
528
        client = GrackleClient('localhost', 8447)
447
538
        self.assertEqual('foo', first_message['message_id'])
448
539
        self.assertEqual('me', first_message['headers']['From'])
449
540
        self.assertEqual('you', first_message['headers']['To'])
450
 
        self.assertEqual(archive['baz'][0]['body'], first_message['body'])
 
541
        self.assertEqual(
 
542
            'abcdefghi\n\nattactment data.', first_message['body'])
451
543
 
452
544
    def test_date_range(self):
453
545
        client = GrackleClient('localhost', 8448)
471
563
        self.assertEqual(['bar', 'naf', 'qux'], ids)
472
564
 
473
565
    def test_date_range_unparsabledaterange(self):
474
 
        client = GrackleClient('localhost', 8449)
 
566
        client = GrackleClient('localhost', 8448)
475
567
        archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
476
568
        with ForkedFakeService.from_client(client, archive):
477
569
            with ExpectedException(UnparsableDateRange, ''):
478
570
                client.get_messages('baz', date_range='2012-01-01')
479
 
 
480
 
    def test_date_range_unparsabledaterange_missing_part(self):
481
 
        client = GrackleClient('localhost', 8450)
482
 
        archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
483
 
        with ForkedFakeService.from_client(client, archive):
484
 
            with ExpectedException(UnparsableDateRange, ''):
485
 
                client.get_messages('baz', date_range='2012-01-01..')
486
 
 
487
 
    def test_date_range_unparsabledaterange_extra_part(self):
488
 
        client = GrackleClient('localhost', 8451)
489
 
        archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
490
 
        with ForkedFakeService.from_client(client, archive):
491
 
            with ExpectedException(UnparsableDateRange, ''):
492
 
                client.get_messages('baz', date_range='2012-01..12-02..12-03')
493
 
 
494
 
 
495
 
class TestHideMessages(TestCase):
496
 
 
497
 
    def test_hide_message_true(self):
498
 
        client = GrackleClient('localhost', 8470)
499
 
        archive = {
500
 
            'baz': [
501
 
                make_message('foo', hidden=False),
502
 
            ]}
503
 
        with ForkedFakeService.from_client(client, archive):
504
 
            response = client.hide_message('baz', 'foo', hidden=True)
505
 
        self.assertEqual('foo', response['message_id'])
506
 
        self.assertIs(True, response['hidden'])
507
 
 
508
 
    def test_hide_message_false(self):
509
 
        client = GrackleClient('localhost', 8470)
510
 
        archive = {
511
 
            'baz': [
512
 
                make_message('foo', hidden=True),
513
 
            ]}
514
 
        with ForkedFakeService.from_client(client, archive):
515
 
            response = client.hide_message('baz', 'foo', hidden=False)
516
 
        self.assertEqual('foo', response['message_id'])
517
 
        self.assertIs(False, response['hidden'])