~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 21:02:32 UTC
  • Revision ID: curtis.hovey@canonical.com-20120317210232-0cw98mbpn9356que
No need to uppercase the reason.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
from BaseHTTPServer import (
2
 
    HTTPServer,
3
 
    BaseHTTPRequestHandler,
4
 
    )
5
1
from email.message import Message
6
2
from email.mime.multipart import MIMEMultipart
7
3
from email.mime.text import MIMEText
8
 
import httplib
9
 
import logging
10
 
import os
11
 
from signal import SIGKILL
12
 
import simplejson
13
4
from StringIO import StringIO
14
 
import sys
15
5
from unittest import TestCase
16
 
from urlparse import urlparse
17
 
from urlparse import parse_qs
18
6
 
19
7
from testtools import ExpectedException
20
8
 
21
 
from grackle.client import (
22
 
    GrackleClient,
 
9
from grackle.client import GrackleClient
 
10
from grackle.error import (
 
11
    ArchiveIdExists,
23
12
    UnparsableDateRange,
24
13
    UnsupportedDisplayType,
25
14
    UnsupportedOrder,
26
15
    )
 
16
from grackle.service import ForkedFakeService
 
17
from grackle.store import (
 
18
    make_json_message,
 
19
    )
27
20
 
28
21
 
29
22
def make_message(message_id, body='body', headers=None, hidden=False):
30
23
    if headers is None:
31
24
        headers = {}
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,
 
25
    message_headers = {
 
26
        'Message-Id': message_id,
 
27
        'date': '2005-01-01',
 
28
        'subject': 'subject',
 
29
        'from': 'author',
 
30
        'replies': '',
44
31
        }
45
 
    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)
46
38
 
47
39
 
48
40
def make_mime_message(message_id, body='body', headers=None, hidden=False,
49
41
                      attachment_type=None):
50
 
    message = MIMEMultipart()
51
 
    message.attach(MIMEText(body))
 
42
    parts = MIMEMultipart()
 
43
    parts.attach(MIMEText(body))
52
44
    if attachment_type is not None:
53
45
        attachment = Message()
54
46
        attachment.set_payload('attactment data.')
55
47
        attachment['Content-Type'] = attachment_type
56
48
        attachment['Content-Disposition'] = 'attachment; filename="file.ext"'
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
179
 
 
180
 
 
181
 
class ForkedFakeService:
182
 
    """A Grackle service fake, as a ContextManager."""
183
 
 
184
 
    def __init__(self, port, messages=None, write_logs=False):
185
 
        """Constructor.
186
 
 
187
 
        :param port: The tcp port to use.
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.
191
 
        :param write_logs: If true, log messages will be written to stdout.
192
 
        """
193
 
        self.pid = None
194
 
        self.port = port
195
 
        if messages is None:
196
 
            self.messages = {}
197
 
        else:
198
 
            self.messages = messages
199
 
        self.read_end, self.write_end = os.pipe()
200
 
        self.write_logs = write_logs
201
 
 
202
 
    @staticmethod
203
 
    def from_client(client, messages=None):
204
 
        """Instantiate a ForkedFakeService from the client.
205
 
 
206
 
        :param port: The client to provide service for.
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.
210
 
        """
211
 
        return ForkedFakeService(client.port, messages)
212
 
 
213
 
    def is_ready(self):
214
 
        """Tell the parent process that the server is ready for writes."""
215
 
        os.write(self.write_end, 'asdf')
216
 
 
217
 
    def __enter__(self):
218
 
        """Run the service.
219
 
 
220
 
        Fork and start a server in the child.  Return when the server is ready
221
 
        for use."""
222
 
        pid = os.fork()
223
 
        if pid == 0:
224
 
            self.start_server()
225
 
        self.pid = pid
226
 
        os.read(self.read_end, 1)
227
 
        return
228
 
 
229
 
    def start_server(self):
230
 
        """Start the HTTP server."""
231
 
        service = HTTPServer(('', self.port), FakeGrackleRequestHandler)
232
 
        service.store = GrackleStore(self.messages)
233
 
        for archive_id, messages in service.store.messages.iteritems():
234
 
            for message in messages:
235
 
                message.setdefault('headers', {})
236
 
        self.is_ready()
237
 
        if self.write_logs:
238
 
            logging.basicConfig(
239
 
                stream=sys.stderr, level=logging.INFO)
240
 
        service.serve_forever()
241
 
 
242
 
    def __exit__(self, exc_type, exc_val, traceback):
243
 
        os.kill(self.pid, SIGKILL)
244
 
 
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
 
 
254
 
class FakeGrackleRequestHandler(BaseHTTPRequestHandler):
255
 
    """A request handler that forwards to server.store."""
256
 
 
257
 
    def __init__(self, *args, **kwargs):
258
 
        """Constructor.  Sets up logging."""
259
 
        self.logger = logging.getLogger('http')
260
 
        BaseHTTPRequestHandler.__init__(self, *args, **kwargs)
261
 
 
262
 
    def do_POST(self):
263
 
        """Create a message on POST."""
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()
269
 
        else:
270
 
            self.send_error(httplib.BAD_REQUEST)
271
 
 
272
 
    def do_GET(self):
273
 
        """Retrieve a list of messages on GET."""
274
 
        scheme, netloc, path, params, query_string, fragments = (
275
 
            urlparse(self.path))
276
 
        parts = path.split('/')
277
 
        if parts[1] == 'archive':
278
 
            try:
279
 
                response = self.server.store.get_messages(
280
 
                    parts[2], query_string)
281
 
                self.send_response(httplib.OK)
282
 
                self.end_headers()
283
 
                self.wfile.write(simplejson.dumps(response))
284
 
            except Exception, error:
285
 
                self.send_response(
286
 
                    httplib.BAD_REQUEST, error.__doc__)
287
 
                return
288
 
 
289
 
    def log_message(self, format, *args):
290
 
        """Override log_message to use standard Python logging."""
291
 
        message = "%s - - [%s] %s\n" % (
292
 
            self.address_string(), self.log_date_time_string(), format % args)
293
 
        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')
294
69
 
295
70
 
296
71
class TestPutMessage(TestCase):
297
72
 
298
73
    def test_put_message(self):
299
 
        client = GrackleClient('localhost', 8436)
300
 
        with ForkedFakeService.from_client(client):
301
 
            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):
302
87
            with ExpectedException(Exception, 'wtf'):
303
 
                client.put_message('arch1', 'asdf',
304
 
                    StringIO('This is not a message'))
 
88
                client.put_message('no-archive', 'id1', StringIO('message'))
305
89
 
306
90
 
307
91
class TestGetMessages(TestCase):
314
98
            sorted(ids), sorted(messages, key=lambda m: m['message_id']))
315
99
 
316
100
    def test_get_messages(self):
317
 
        client = GrackleClient('localhost', 8435)
 
101
        client = GrackleClient('localhost', 8430)
318
102
        archive = {
319
103
            'baz': [make_message('foo'), make_message('bar')]}
320
104
        with ForkedFakeService.from_client(client, archive):
348
132
 
349
133
    def get_messages_member_order_test(self, key):
350
134
        client = GrackleClient('localhost', 8439)
 
135
        if key == 'author':
 
136
            header_name = 'from'
 
137
        else:
 
138
            header_name = key
351
139
        archive = {
352
140
            'baz': [
353
 
                make_message('foo', headers={key: '2011-03-25'}),
354
 
                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'}),
355
143
             ]}
356
144
        with ForkedFakeService.from_client(client, archive):
357
145
            response = client.get_messages('baz')
504
292
        first_message = response['messages'][0]
505
293
        self.assertEqual('foo', first_message['message_id'])
506
294
        self.assertEqual(
507
 
            {'From': 'me', 'Message-Id': 'foo', 'To': 'you'},
508
 
            first_message['headers'])
 
295
            archive['baz'][0]['headers'], first_message['headers'])
509
296
        self.assertNotIn('body', first_message)
510
297
 
511
298
    def test_display_type_text_only(self):
522
309
        self.assertEqual('foo', first_message['message_id'])
523
310
        self.assertEqual('me', first_message['headers']['From'])
524
311
        self.assertEqual('you', first_message['headers']['To'])
525
 
        self.assertEqual('abcdefghi', first_message['body'])
 
312
        self.assertEqual(archive['baz'][0]['body'], first_message['body'])
526
313
 
527
314
    def test_display_type_all(self):
528
315
        client = GrackleClient('localhost', 8447)
538
325
        self.assertEqual('foo', first_message['message_id'])
539
326
        self.assertEqual('me', first_message['headers']['From'])
540
327
        self.assertEqual('you', first_message['headers']['To'])
541
 
        self.assertEqual(
542
 
            'abcdefghi\n\nattactment data.', first_message['body'])
 
328
        self.assertEqual(archive['baz'][0]['body'], first_message['body'])
543
329
 
544
330
    def test_date_range(self):
545
331
        client = GrackleClient('localhost', 8448)
563
349
        self.assertEqual(['bar', 'naf', 'qux'], ids)
564
350
 
565
351
    def test_date_range_unparsabledaterange(self):
566
 
        client = GrackleClient('localhost', 8448)
 
352
        client = GrackleClient('localhost', 8449)
567
353
        archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
568
354
        with ForkedFakeService.from_client(client, archive):
569
355
            with ExpectedException(UnparsableDateRange, ''):
570
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'])