~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:01:20 UTC
  • mto: This revision was merged to the branch mainline in revision 37.
  • Revision ID: curtis.hovey@canonical.com-20120131050120-6crizbaw5vn39d4w
Removed hack now that test data guarantees a sane message dict.

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
        'body': body,
43
42
        }
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)
 
43
    if 'in-reply-to' in headers:
 
44
        message['replies'] = headers['in-reply-to']
 
45
    return message
50
46
 
51
47
 
52
48
def make_mime_message(message_id, body='body', headers=None, hidden=False,
53
49
                      attachment_type=None):
54
 
    parts = MIMEMultipart()
55
 
    parts.attach(MIMEText(body))
 
50
    message = MIMEMultipart()
 
51
    message.attach(MIMEText(body))
56
52
    if attachment_type is not None:
57
53
        attachment = Message()
58
54
        attachment.set_payload('attactment data.')
59
55
        attachment['Content-Type'] = attachment_type
60
56
        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:
 
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
    def get_messages(self, archive_id, query_string):
 
84
        """Return matching messages.
 
85
 
 
86
        :param archive_id: The archive to retrieve from.
 
87
        :param query_string: Contains 'parameters', which is a JSON-format
 
88
            string describing parameters.
 
89
        """
 
90
        query = parse_qs(query_string)
 
91
        parameters = simplejson.loads(query['parameters'][0])
 
92
        order = parameters.get('order')
 
93
        messages = self.messages[archive_id]
 
94
        if order is not None:
 
95
            if order not in SUPPORTED_ORDERS:
 
96
                raise UnsupportedOrder
 
97
            elif order.startswith('thread_'):
 
98
                threaded = threaded_messages(messages)
 
99
                messages = []
 
100
                if order == 'thread_subject':
 
101
                    threaded.sort(key=lambda t: t[0]['subject'])
 
102
                if order == 'thread_oldest':
 
103
                    threaded.sort(key=lambda t: min(m['date'] for m in t))
 
104
                if order == 'thread_newest':
 
105
                    threaded.sort(key=lambda t: max(m['date'] for m in t))
 
106
                for thread in threaded:
 
107
                    messages.extend(thread)
 
108
            else:
 
109
                messages.sort(key=lambda m: m[order])
 
110
        display_type = parameters.get('display_type', 'all')
 
111
        if display_type not in SUPPORTED_DISPLAY_TYPES:
 
112
            raise UnsupportedDisplayType
 
113
        new_messages = []
 
114
        for message in messages:
 
115
            if (not parameters['include_hidden']
 
116
                and message.get('hidden', False)):
 
117
                continue
 
118
 
 
119
            if ('message_ids' in parameters
 
120
                and message['message_id'] not in parameters['message_ids']):
 
121
                continue
 
122
            message = dict(message)
 
123
            if 'headers' in parameters:
 
124
                headers = dict(
 
125
                    (k, v) for k, v in message['headers'].iteritems()
 
126
                    if k in parameters['headers'])
 
127
                message['headers'] = headers
 
128
            max_body = parameters.get('max_body_length')
 
129
            if display_type == 'headers-only':
 
130
                del message['body']
 
131
            elif (display_type == 'text-only'
 
132
                  and isinstance(message['body'], list)):
 
133
                text_parts = [
 
134
                    part.get_payload() for part in message['body']
 
135
                    if part.get_content_type() == 'text/plain']
 
136
                message['body'] = '\n\n'.join(text_parts)
 
137
            elif (display_type == 'all'
 
138
                  and isinstance(message['body'], list)):
 
139
                parts = [str(part.get_payload()) for part in message['body']]
 
140
                message['body'] = '\n\n'.join(parts)
 
141
            if max_body is not None and display_type != 'headers-only':
 
142
                message['body'] = message['body'][:max_body]
 
143
            new_messages.append(message)
 
144
        messages = new_messages
 
145
        limit = parameters.get('limit', 100)
 
146
        memo = parameters.get('memo')
 
147
        message_id_indices = dict(
 
148
            (m['message_id'], idx) for idx, m in enumerate(messages))
 
149
        if memo is None:
 
150
            start = 0
 
151
        else:
 
152
            start = message_id_indices[memo.encode('rot13')]
 
153
        if start > 0:
 
154
            previous_memo = messages[start - 1]['message_id'].encode('rot13')
 
155
        else:
 
156
            previous_memo = None
 
157
        end = min(start + limit, len(messages))
 
158
        if end < len(messages):
 
159
            next_memo = messages[end]['message_id'].encode('rot13')
 
160
        else:
 
161
            next_memo = None
 
162
        messages = messages[start:end]
 
163
 
 
164
        response = {
 
165
            'messages': messages,
 
166
            'next_memo': next_memo,
 
167
            'previous_memo': previous_memo
 
168
            }
 
169
        return response
 
170
 
 
171
 
 
172
class ForkedFakeService:
66
173
    """A Grackle service fake, as a ContextManager."""
67
174
 
68
 
    def __init__(self, port, message_archives=None, write_logs=False):
 
175
    def __init__(self, port, messages=None, write_logs=False):
69
176
        """Constructor.
70
177
 
71
178
        :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.
 
179
        :param messages: A dict of lists of dicts representing messages.  The
 
180
            outer dict represents the archive, the list represents the list of
 
181
            messages for that archive.
75
182
        :param write_logs: If true, log messages will be written to stdout.
76
183
        """
77
184
        self.pid = None
78
185
        self.port = port
79
 
        if message_archives is None:
80
 
            self.message_archives = {}
 
186
        if messages is None:
 
187
            self.messages = {}
81
188
        else:
82
 
            self.message_archives = message_archives
 
189
            self.messages = messages
83
190
        self.read_end, self.write_end = os.pipe()
84
191
        self.write_logs = write_logs
85
192
 
86
193
    @staticmethod
87
 
    def from_client(client, message_archives=None):
 
194
    def from_client(client, messages=None):
88
195
        """Instantiate a ForkedFakeService from the client.
89
196
 
90
197
        :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.
 
198
        :param messages: A dict of lists of dicts representing messages.  The
 
199
            outer dict represents the archive, the list represents the list of
 
200
            messages for that archive.
94
201
        """
95
 
        return ForkedFakeService(client.port, message_archives)
 
202
        return ForkedFakeService(client.port, messages)
96
203
 
97
204
    def is_ready(self):
98
205
        """Tell the parent process that the server is ready for writes."""
113
220
    def start_server(self):
114
221
        """Start the HTTP server."""
115
222
        service = HTTPServer(('', self.port), FakeGrackleRequestHandler)
116
 
        service.store = MemoryStore(self.message_archives)
 
223
        service.store = GrackleStore(self.messages)
 
224
        for archive_id, messages in service.store.messages.iteritems():
 
225
            for message in messages:
 
226
                message.setdefault('headers', {})
117
227
        self.is_ready()
118
228
        if self.write_logs:
119
229
            logging.basicConfig(
124
234
        os.kill(self.pid, SIGKILL)
125
235
 
126
236
 
 
237
SUPPORTED_DISPLAY_TYPES = set(['all', 'text-only', 'headers-only'])
 
238
 
 
239
 
 
240
SUPPORTED_ORDERS = set(
 
241
    ['date', 'author', 'subject', 'thread_newest', 'thread_oldest',
 
242
     'thread_subject'])
 
243
 
 
244
 
127
245
class FakeGrackleRequestHandler(BaseHTTPRequestHandler):
128
246
    """A request handler that forwards to server.store."""
129
247
 
132
250
        self.logger = logging.getLogger('http')
133
251
        BaseHTTPRequestHandler.__init__(self, *args, **kwargs)
134
252
 
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
253
    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)
 
254
        """Create a message on POST."""
 
255
        message = self.rfile.read(int(self.headers['content-length']))
 
256
        if message == 'This is a message':
 
257
            self.send_response(httplib.CREATED)
 
258
            self.end_headers()
 
259
            self.wfile.close()
 
260
        else:
 
261
            self.send_error(httplib.BAD_REQUEST)
183
262
 
184
263
    def do_GET(self):
185
264
        """Retrieve a list of messages on GET."""
193
272
                self.send_response(httplib.OK)
194
273
                self.end_headers()
195
274
                self.wfile.write(simplejson.dumps(response))
196
 
            except Exception, error:
197
 
                self.send_response(
198
 
                    httplib.BAD_REQUEST, error.__doc__)
 
275
            except UnsupportedOrder:
 
276
                self.send_response(
 
277
                    httplib.BAD_REQUEST, UnsupportedOrder.__doc__)
 
278
                return
 
279
            except UnsupportedDisplayType:
 
280
                self.send_response(
 
281
                    httplib.BAD_REQUEST, UnsupportedDisplayType.__doc__)
199
282
                return
200
283
 
201
284
    def log_message(self, format, *args):
205
288
        self.logger.info(message)
206
289
 
207
290
 
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
291
class TestPutMessage(TestCase):
227
292
 
228
293
    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):
 
294
        client = GrackleClient('localhost', 8436)
 
295
        with ForkedFakeService.from_client(client):
 
296
            client.put_message('arch1', 'asdf', StringIO('This is a message'))
242
297
            with ExpectedException(Exception, 'wtf'):
243
 
                client.put_message('no-archive', 'id1', StringIO('message'))
 
298
                client.put_message('arch1', 'asdf',
 
299
                    StringIO('This is not a message'))
244
300
 
245
301
 
246
302
class TestGetMessages(TestCase):
253
309
            sorted(ids), sorted(messages, key=lambda m: m['message_id']))
254
310
 
255
311
    def test_get_messages(self):
256
 
        client = GrackleClient('localhost', 8430)
 
312
        client = GrackleClient('localhost', 8435)
257
313
        archive = {
258
314
            'baz': [make_message('foo'), make_message('bar')]}
259
315
        with ForkedFakeService.from_client(client, archive):
287
343
 
288
344
    def get_messages_member_order_test(self, key):
289
345
        client = GrackleClient('localhost', 8439)
290
 
        if key == 'author':
291
 
            header_name = 'from'
292
 
        else:
293
 
            header_name = key
294
346
        archive = {
295
347
            'baz': [
296
 
                make_message('foo', headers={header_name: '2011-03-25'}),
297
 
                make_message('bar', headers={header_name: '2011-03-24'}),
 
348
                make_message('foo', headers={key: '2011-03-25'}),
 
349
                make_message('bar', headers={key: '2011-03-24'}),
298
350
             ]}
299
351
        with ForkedFakeService.from_client(client, archive):
300
352
            response = client.get_messages('baz')
447
499
        first_message = response['messages'][0]
448
500
        self.assertEqual('foo', first_message['message_id'])
449
501
        self.assertEqual(
450
 
            archive['baz'][0]['headers'], first_message['headers'])
 
502
            {'From': 'me', 'Message-Id': 'foo', 'To': 'you'},
 
503
            first_message['headers'])
451
504
        self.assertNotIn('body', first_message)
452
505
 
453
506
    def test_display_type_text_only(self):
464
517
        self.assertEqual('foo', first_message['message_id'])
465
518
        self.assertEqual('me', first_message['headers']['From'])
466
519
        self.assertEqual('you', first_message['headers']['To'])
467
 
        self.assertEqual(archive['baz'][0]['body'], first_message['body'])
 
520
        self.assertEqual('abcdefghi', first_message['body'])
468
521
 
469
522
    def test_display_type_all(self):
470
523
        client = GrackleClient('localhost', 8447)
480
533
        self.assertEqual('foo', first_message['message_id'])
481
534
        self.assertEqual('me', first_message['headers']['From'])
482
535
        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'])
 
536
        self.assertEqual(
 
537
            'abcdefghi\n\nattactment data.', first_message['body'])