~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:
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
 
20
 
from grackle.client import GrackleClient
21
 
from grackle.error import (
22
 
    ArchiveIdExists,
23
 
    UnparsableDateRange,
 
21
from grackle.client import (
 
22
    GrackleClient,
24
23
    UnsupportedDisplayType,
25
24
    UnsupportedOrder,
26
25
    )
27
 
from grackle.service import ForkedFakeService
28
 
from grackle.store import (
29
 
    make_json_message,
30
 
    MemoryStore,
31
 
    )
32
26
 
33
27
 
34
28
def make_message(message_id, body='body', headers=None, hidden=False):
35
29
    if headers is None:
36
30
        headers = {}
37
 
    message_headers = {
38
 
        'Message-Id': message_id,
39
 
        'date': '2005-01-01',
40
 
        'subject': 'subject',
41
 
        'from': 'author',
42
 
        '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,
43
43
        }
44
 
    message_headers.update(headers.items())
45
 
    message = Message()
46
 
    message.set_payload(body)
47
 
    for key, value in message_headers.items():
48
 
        message[key] = value
49
 
    return make_json_message(message_id, message.as_string(), hidden)
 
44
    return message
50
45
 
51
46
 
52
47
def make_mime_message(message_id, body='body', headers=None, hidden=False,
53
48
                      attachment_type=None):
54
 
    parts = MIMEMultipart()
55
 
    parts.attach(MIMEText(body))
 
49
    message = MIMEMultipart()
 
50
    message.attach(MIMEText(body))
56
51
    if attachment_type is not None:
57
52
        attachment = Message()
58
53
        attachment.set_payload('attactment data.')
59
54
        attachment['Content-Type'] = attachment_type
60
55
        attachment['Content-Disposition'] = 'attachment; filename="file.ext"'
61
 
        parts.attach(attachment)
62
 
    return make_message(message_id, parts.as_string(), headers, hidden)
63
 
 
64
 
 
65
 
class XXXForkedFakeService:
 
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:
66
172
    """A Grackle service fake, as a ContextManager."""
67
173
 
68
 
    def __init__(self, port, message_archives=None, write_logs=False):
 
174
    def __init__(self, port, messages=None, write_logs=False):
69
175
        """Constructor.
70
176
 
71
177
        :param port: The tcp port to use.
72
 
        :param message_archives: A dict of lists of dicts representing
73
 
            archives of messages. The outer dict represents the archive,
74
 
            the list represents the list of messages for that archive.
 
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.
75
181
        :param write_logs: If true, log messages will be written to stdout.
76
182
        """
77
183
        self.pid = None
78
184
        self.port = port
79
 
        if message_archives is None:
80
 
            self.message_archives = {}
 
185
        if messages is None:
 
186
            self.messages = {}
81
187
        else:
82
 
            self.message_archives = message_archives
 
188
            self.messages = messages
83
189
        self.read_end, self.write_end = os.pipe()
84
190
        self.write_logs = write_logs
85
191
 
86
192
    @staticmethod
87
 
    def from_client(client, message_archives=None):
 
193
    def from_client(client, messages=None):
88
194
        """Instantiate a ForkedFakeService from the client.
89
195
 
90
196
        :param port: The client to provide service for.
91
 
        :param message_archives: A dict of lists of dicts representing
92
 
            archives of messages. The outer dict represents the archive,
93
 
            the list represents the list of messages for that archive.
 
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.
94
200
        """
95
 
        return ForkedFakeService(client.port, message_archives)
 
201
        return ForkedFakeService(client.port, messages)
96
202
 
97
203
    def is_ready(self):
98
204
        """Tell the parent process that the server is ready for writes."""
113
219
    def start_server(self):
114
220
        """Start the HTTP server."""
115
221
        service = HTTPServer(('', self.port), FakeGrackleRequestHandler)
116
 
        service.store = MemoryStore(self.message_archives)
 
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', {})
117
226
        self.is_ready()
118
227
        if self.write_logs:
119
228
            logging.basicConfig(
124
233
        os.kill(self.pid, SIGKILL)
125
234
 
126
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
 
127
244
class FakeGrackleRequestHandler(BaseHTTPRequestHandler):
128
245
    """A request handler that forwards to server.store."""
129
246
 
132
249
        self.logger = logging.getLogger('http')
133
250
        BaseHTTPRequestHandler.__init__(self, *args, **kwargs)
134
251
 
135
 
    def do_PUT(self):
136
 
        """Create an archive or message on PUT."""
137
 
        scheme, netloc, path, params, query_string, fragments = (
138
 
            urlparse(self.path))
139
 
        parts = path.split('/')
140
 
        if parts[1] != 'archive':
141
 
            # This is an unknonwn operation?
142
 
            return
143
 
        if len(parts) == 3:
144
 
            # This expected path is /archive/archive_id.
145
 
            try:
146
 
                self.server.store.put_archive(parts[2])
147
 
                self.send_response(httplib.CREATED)
148
 
                self.end_headers()
149
 
                self.wfile.close()
150
 
            except Exception, error:
151
 
                self.send_response(
152
 
                    httplib.BAD_REQUEST, error.__doc__)
153
 
        if len(parts) == 4:
154
 
            # This expected path is /archive/archive_id/message_id.
155
 
            try:
156
 
                message = self.rfile.read(int(self.headers['content-length']))
157
 
                self.server.store.put_message(parts[2], parts[3], message)
158
 
                self.send_response(httplib.CREATED)
159
 
                self.end_headers()
160
 
                self.wfile.close()
161
 
            except:
162
 
                self.send_error(httplib.BAD_REQUEST)
163
 
 
164
252
    def do_POST(self):
165
 
        """Change a message on POST."""
166
 
        scheme, netloc, path, params, query_string, fragments = (
167
 
            urlparse(self.path))
168
 
        parts = path.split('/')
169
 
        if parts[1] != 'archive':
170
 
            # This is an unknonwn operation?
171
 
            return
172
 
        if len(parts) == 4:
173
 
            # This expected path is /archive/archive_id/message_id.
174
 
            try:
175
 
                # This expected path is /archive/archive_id/message_id.
176
 
                response = self.server.store.hide_message(
177
 
                    parts[2], parts[3], query_string)
178
 
                self.send_response(httplib.OK)
179
 
                self.end_headers()
180
 
                self.wfile.write(simplejson.dumps(response))
181
 
            except:
182
 
                self.send_error(httplib.BAD_REQUEST)
 
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)
183
261
 
184
262
    def do_GET(self):
185
263
        """Retrieve a list of messages on GET."""
193
271
                self.send_response(httplib.OK)
194
272
                self.end_headers()
195
273
                self.wfile.write(simplejson.dumps(response))
196
 
            except Exception, error:
197
 
                self.send_response(
198
 
                    httplib.BAD_REQUEST, error.__doc__)
 
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__)
199
281
                return
200
282
 
201
283
    def log_message(self, format, *args):
205
287
        self.logger.info(message)
206
288
 
207
289
 
208
 
class TestPutArchive(TestCase):
209
 
 
210
 
    def test_put_archive(self):
211
 
        client = GrackleClient('localhost', 8410)
212
 
        message_archives = {}
213
 
        with ForkedFakeService.from_client(client, message_archives):
214
 
            client.put_archive('arch1')
215
 
            response = client.get_messages('arch1')
216
 
        self.assertEqual(0, len(response['messages']))
217
 
 
218
 
    def test_put_archive_existing_archive(self):
219
 
        client = GrackleClient('localhost', 8411)
220
 
        message_archives = {'arch1': []}
221
 
        with ForkedFakeService.from_client(client, message_archives):
222
 
            with ExpectedException(ArchiveIdExists, ''):
223
 
                client.put_archive('arch1')
224
 
 
225
 
 
226
290
class TestPutMessage(TestCase):
227
291
 
228
292
    def test_put_message(self):
229
 
        client = GrackleClient('localhost', 8420)
230
 
        message_archives = {'arch1': []}
231
 
        with ForkedFakeService.from_client(client, message_archives):
232
 
            client.put_message('arch1', 'id1', StringIO('This is a message'))
233
 
            response = client.get_messages('arch1')
234
 
        self.assertEqual(1, len(response['messages']))
235
 
        message = response['messages'][0]
236
 
        self.assertEqual('id1', message['message_id'])
237
 
 
238
 
    def test_put_message_without_archive(self):
239
 
        client = GrackleClient('localhost', 8421)
240
 
        message_archives = {'arch1': []}
241
 
        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'))
242
296
            with ExpectedException(Exception, 'wtf'):
243
 
                client.put_message('no-archive', 'id1', StringIO('message'))
 
297
                client.put_message('arch1', 'asdf',
 
298
                    StringIO('This is not a message'))
244
299
 
245
300
 
246
301
class TestGetMessages(TestCase):
253
308
            sorted(ids), sorted(messages, key=lambda m: m['message_id']))
254
309
 
255
310
    def test_get_messages(self):
256
 
        client = GrackleClient('localhost', 8430)
 
311
        client = GrackleClient('localhost', 8435)
257
312
        archive = {
258
313
            'baz': [make_message('foo'), make_message('bar')]}
259
314
        with ForkedFakeService.from_client(client, archive):
287
342
 
288
343
    def get_messages_member_order_test(self, key):
289
344
        client = GrackleClient('localhost', 8439)
290
 
        if key == 'author':
291
 
            header_name = 'from'
292
 
        else:
293
 
            header_name = key
294
345
        archive = {
295
346
            'baz': [
296
 
                make_message('foo', headers={header_name: '2011-03-25'}),
297
 
                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'}),
298
349
             ]}
299
350
        with ForkedFakeService.from_client(client, archive):
300
351
            response = client.get_messages('baz')
447
498
        first_message = response['messages'][0]
448
499
        self.assertEqual('foo', first_message['message_id'])
449
500
        self.assertEqual(
450
 
            archive['baz'][0]['headers'], first_message['headers'])
 
501
            {'From': 'me', 'Message-Id': 'foo', 'To': 'you'},
 
502
            first_message['headers'])
451
503
        self.assertNotIn('body', first_message)
452
504
 
453
505
    def test_display_type_text_only(self):
464
516
        self.assertEqual('foo', first_message['message_id'])
465
517
        self.assertEqual('me', first_message['headers']['From'])
466
518
        self.assertEqual('you', first_message['headers']['To'])
467
 
        self.assertEqual(archive['baz'][0]['body'], first_message['body'])
 
519
        self.assertEqual('abcdefghi', first_message['body'])
468
520
 
469
521
    def test_display_type_all(self):
470
522
        client = GrackleClient('localhost', 8447)
480
532
        self.assertEqual('foo', first_message['message_id'])
481
533
        self.assertEqual('me', first_message['headers']['From'])
482
534
        self.assertEqual('you', first_message['headers']['To'])
483
 
        self.assertEqual(archive['baz'][0]['body'], first_message['body'])
484
 
 
485
 
    def test_date_range(self):
486
 
        client = GrackleClient('localhost', 8448)
487
 
        archive = {
488
 
            'baz': [
489
 
                make_mime_message(
490
 
                    'foo', 'abcdefghi', headers={'date': '2011-12-31'}),
491
 
                make_mime_message(
492
 
                    'bar', 'abcdefghi', headers={'date': '2012-01-01'}),
493
 
                make_mime_message(
494
 
                    'qux', 'abcdefghi', headers={'date': '2012-01-15'}),
495
 
                make_mime_message(
496
 
                    'naf', 'abcdefghi', headers={'date': '2012-01-31'}),
497
 
                make_mime_message(
498
 
                    'doh', 'abcdefghi', headers={'date': '2012-02-01'}),
499
 
                    ]}
500
 
        with ForkedFakeService.from_client(client, archive):
501
 
            response = client.get_messages(
502
 
                'baz', date_range='2012-01-01..2012-01-31')
503
 
        ids = sorted(m['message_id'] for m in response['messages'])
504
 
        self.assertEqual(['bar', 'naf', 'qux'], ids)
505
 
 
506
 
    def test_date_range_unparsabledaterange(self):
507
 
        client = GrackleClient('localhost', 8449)
508
 
        archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
509
 
        with ForkedFakeService.from_client(client, archive):
510
 
            with ExpectedException(UnparsableDateRange, ''):
511
 
                client.get_messages('baz', date_range='2012-01-01')
512
 
 
513
 
    def test_date_range_unparsabledaterange_missing_part(self):
514
 
        client = GrackleClient('localhost', 8450)
515
 
        archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
516
 
        with ForkedFakeService.from_client(client, archive):
517
 
            with ExpectedException(UnparsableDateRange, ''):
518
 
                client.get_messages('baz', date_range='2012-01-01..')
519
 
 
520
 
    def test_date_range_unparsabledaterange_extra_part(self):
521
 
        client = GrackleClient('localhost', 8451)
522
 
        archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
523
 
        with ForkedFakeService.from_client(client, archive):
524
 
            with ExpectedException(UnparsableDateRange, ''):
525
 
                client.get_messages('baz', date_range='2012-01..12-02..12-03')
526
 
 
527
 
 
528
 
class TestHideMessages(TestCase):
529
 
 
530
 
    def test_hide_message_true(self):
531
 
        client = GrackleClient('localhost', 8470)
532
 
        archive = {
533
 
            'baz': [
534
 
                make_message('foo', hidden=False),
535
 
            ]}
536
 
        with ForkedFakeService.from_client(client, archive):
537
 
            response = client.hide_message('baz', 'foo', hidden=True)
538
 
        self.assertEqual('foo', response['message_id'])
539
 
        self.assertIs(True, response['hidden'])
540
 
 
541
 
    def test_hide_message_false(self):
542
 
        client = GrackleClient('localhost', 8470)
543
 
        archive = {
544
 
            'baz': [
545
 
                make_message('foo', hidden=True),
546
 
            ]}
547
 
        with ForkedFakeService.from_client(client, archive):
548
 
            response = client.hide_message('baz', 'foo', hidden=False)
549
 
        self.assertEqual('foo', response['message_id'])
550
 
        self.assertIs(False, response['hidden'])
 
535
        self.assertEqual(
 
536
            'abcdefghi\n\nattactment data.', first_message['body'])