~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
 
136
262
    def do_POST(self):
137
263
        """Create a message on POST."""
138
264
        message = self.rfile.read(int(self.headers['content-length']))
139
 
        scheme, netloc, path, params, query_string, fragments = (
140
 
            urlparse(self.path))
141
 
        parts = path.split('/')
142
 
        if parts[1] == 'archive' and len(parts) == 4:
143
 
            try:
144
 
                # This expected path is /archive/archive_id/message_id.
145
 
                self.server.store.put_message(parts[2], parts[3], message)
146
 
                self.send_response(httplib.CREATED)
147
 
                self.end_headers()
148
 
                self.wfile.close()
149
 
            except:
150
 
                self.send_error(httplib.BAD_REQUEST)
 
265
        if message == 'This is a message':
 
266
            self.send_response(httplib.CREATED)
 
267
            self.end_headers()
 
268
            self.wfile.close()
 
269
        else:
 
270
            self.send_error(httplib.BAD_REQUEST)
151
271
 
152
272
    def do_GET(self):
153
273
        """Retrieve a list of messages on GET."""
176
296
class TestPutMessage(TestCase):
177
297
 
178
298
    def test_put_message(self):
179
 
        client = GrackleClient('localhost', 8420)
180
 
        message_archives = {'arch1': []}
181
 
        with ForkedFakeService.from_client(client, message_archives):
182
 
            client.put_message('arch1', 'id1', StringIO('This is a message'))
183
 
            response = client.get_messages('arch1')
184
 
        self.assertEqual(1, len(response['messages']))
185
 
        message = response['messages'][0]
186
 
        self.assertEqual('id1', message['message_id'])
187
 
 
188
 
    def test_put_message_without_archive(self):
189
 
        client = GrackleClient('localhost', 8421)
190
 
        message_archives = {'arch1': []}
191
 
        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'))
192
302
            with ExpectedException(Exception, 'wtf'):
193
 
                client.put_message('no-archive', 'id1', StringIO('message'))
 
303
                client.put_message('arch1', 'asdf',
 
304
                    StringIO('This is not a message'))
194
305
 
195
306
 
196
307
class TestGetMessages(TestCase):
203
314
            sorted(ids), sorted(messages, key=lambda m: m['message_id']))
204
315
 
205
316
    def test_get_messages(self):
206
 
        client = GrackleClient('localhost', 8430)
 
317
        client = GrackleClient('localhost', 8435)
207
318
        archive = {
208
319
            'baz': [make_message('foo'), make_message('bar')]}
209
320
        with ForkedFakeService.from_client(client, archive):
237
348
 
238
349
    def get_messages_member_order_test(self, key):
239
350
        client = GrackleClient('localhost', 8439)
240
 
        if key == 'author':
241
 
            header_name = 'from'
242
 
        else:
243
 
            header_name = key
244
351
        archive = {
245
352
            'baz': [
246
 
                make_message('foo', headers={header_name: '2011-03-25'}),
247
 
                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'}),
248
355
             ]}
249
356
        with ForkedFakeService.from_client(client, archive):
250
357
            response = client.get_messages('baz')
397
504
        first_message = response['messages'][0]
398
505
        self.assertEqual('foo', first_message['message_id'])
399
506
        self.assertEqual(
400
 
            archive['baz'][0]['headers'], first_message['headers'])
 
507
            {'From': 'me', 'Message-Id': 'foo', 'To': 'you'},
 
508
            first_message['headers'])
401
509
        self.assertNotIn('body', first_message)
402
510
 
403
511
    def test_display_type_text_only(self):
414
522
        self.assertEqual('foo', first_message['message_id'])
415
523
        self.assertEqual('me', first_message['headers']['From'])
416
524
        self.assertEqual('you', first_message['headers']['To'])
417
 
        self.assertEqual(archive['baz'][0]['body'], first_message['body'])
 
525
        self.assertEqual('abcdefghi', first_message['body'])
418
526
 
419
527
    def test_display_type_all(self):
420
528
        client = GrackleClient('localhost', 8447)
430
538
        self.assertEqual('foo', first_message['message_id'])
431
539
        self.assertEqual('me', first_message['headers']['From'])
432
540
        self.assertEqual('you', first_message['headers']['To'])
433
 
        self.assertEqual(archive['baz'][0]['body'], first_message['body'])
 
541
        self.assertEqual(
 
542
            'abcdefghi\n\nattactment data.', first_message['body'])
434
543
 
435
544
    def test_date_range(self):
436
545
        client = GrackleClient('localhost', 8448)
454
563
        self.assertEqual(['bar', 'naf', 'qux'], ids)
455
564
 
456
565
    def test_date_range_unparsabledaterange(self):
457
 
        client = GrackleClient('localhost', 8449)
 
566
        client = GrackleClient('localhost', 8448)
458
567
        archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
459
568
        with ForkedFakeService.from_client(client, archive):
460
569
            with ExpectedException(UnparsableDateRange, ''):
461
570
                client.get_messages('baz', date_range='2012-01-01')
462
 
 
463
 
    def test_date_range_unparsabledaterange_missing_part(self):
464
 
        client = GrackleClient('localhost', 8450)
465
 
        archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
466
 
        with ForkedFakeService.from_client(client, archive):
467
 
            with ExpectedException(UnparsableDateRange, ''):
468
 
                client.get_messages('baz', date_range='2012-01-01..')
469
 
 
470
 
    def test_date_range_unparsabledaterange_extra_part(self):
471
 
        client = GrackleClient('localhost', 8451)
472
 
        archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
473
 
        with ForkedFakeService.from_client(client, archive):
474
 
            with ExpectedException(UnparsableDateRange, ''):
475
 
                client.get_messages('baz', date_range='2012-01..12-02..12-03')