~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:21:37 UTC
  • Revision ID: curtis.hovey@canonical.com-20120317212137-r7zonl5ksenpa5ci
Save the scheme.

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
 
                if not start_date or not end_date:
121
 
                    raise UnparsableDateRange
122
 
            except ValueError:
123
 
                raise UnparsableDateRange
124
 
        new_messages = []
125
 
        for message in messages:
126
 
            if (not parameters['include_hidden'] and message['hidden']):
127
 
                continue
128
 
            if ('message_ids' in parameters
129
 
                and message['message_id'] not in parameters['message_ids']):
130
 
                continue
131
 
            if ('date_range' in parameters
132
 
                and (message['date'] < start_date
133
 
                     or message['date'] > end_date)):
134
 
                continue
135
 
            message = dict(message)
136
 
            if 'headers' in parameters:
137
 
                headers = dict(
138
 
                    (k, v) for k, v in message['headers'].iteritems()
139
 
                    if k in parameters['headers'])
140
 
                message['headers'] = headers
141
 
            if display_type == 'headers-only':
142
 
                del message['body']
143
 
            elif display_type == 'text-only' and self.is_multipart(message):
144
 
                text_parts = [
145
 
                    part.get_payload() for part in message['body']
146
 
                    if part.get_content_type() == 'text/plain']
147
 
                message['body'] = '\n\n'.join(text_parts)
148
 
            elif display_type == 'all' and self.is_multipart(message):
149
 
                parts = [str(part.get_payload()) for part in message['body']]
150
 
                message['body'] = '\n\n'.join(parts)
151
 
            max_body = parameters.get('max_body_length')
152
 
            if max_body is not None and display_type != 'headers-only':
153
 
                message['body'] = message['body'][:max_body]
154
 
            new_messages.append(message)
155
 
        messages = new_messages
156
 
        limit = parameters.get('limit', 100)
157
 
        memo = parameters.get('memo')
158
 
        message_id_indices = dict(
159
 
            (m['message_id'], idx) for idx, m in enumerate(messages))
160
 
        if memo is None:
161
 
            start = 0
162
 
        else:
163
 
            start = message_id_indices[memo.encode('rot13')]
164
 
        if start > 0:
165
 
            previous_memo = messages[start - 1]['message_id'].encode('rot13')
166
 
        else:
167
 
            previous_memo = None
168
 
        end = min(start + limit, len(messages))
169
 
        if end < len(messages):
170
 
            next_memo = messages[end]['message_id'].encode('rot13')
171
 
        else:
172
 
            next_memo = None
173
 
        messages = messages[start:end]
174
 
 
175
 
        response = {
176
 
            'messages': messages,
177
 
            'next_memo': next_memo,
178
 
            'previous_memo': previous_memo
179
 
            }
180
 
        return response
181
 
 
182
 
 
183
 
class ForkedFakeService:
184
 
    """A Grackle service fake, as a ContextManager."""
185
 
 
186
 
    def __init__(self, port, messages=None, write_logs=False):
187
 
        """Constructor.
188
 
 
189
 
        :param port: The tcp port to use.
190
 
        :param messages: A dict of lists of dicts representing messages.  The
191
 
            outer dict represents the archive, the list represents the list of
192
 
            messages for that archive.
193
 
        :param write_logs: If true, log messages will be written to stdout.
194
 
        """
195
 
        self.pid = None
196
 
        self.port = port
197
 
        if messages is None:
198
 
            self.messages = {}
199
 
        else:
200
 
            self.messages = messages
201
 
        self.read_end, self.write_end = os.pipe()
202
 
        self.write_logs = write_logs
203
 
 
204
 
    @staticmethod
205
 
    def from_client(client, messages=None):
206
 
        """Instantiate a ForkedFakeService from the client.
207
 
 
208
 
        :param port: The client to provide service for.
209
 
        :param messages: A dict of lists of dicts representing messages.  The
210
 
            outer dict represents the archive, the list represents the list of
211
 
            messages for that archive.
212
 
        """
213
 
        return ForkedFakeService(client.port, messages)
214
 
 
215
 
    def is_ready(self):
216
 
        """Tell the parent process that the server is ready for writes."""
217
 
        os.write(self.write_end, 'asdf')
218
 
 
219
 
    def __enter__(self):
220
 
        """Run the service.
221
 
 
222
 
        Fork and start a server in the child.  Return when the server is ready
223
 
        for use."""
224
 
        pid = os.fork()
225
 
        if pid == 0:
226
 
            self.start_server()
227
 
        self.pid = pid
228
 
        os.read(self.read_end, 1)
229
 
        return
230
 
 
231
 
    def start_server(self):
232
 
        """Start the HTTP server."""
233
 
        service = HTTPServer(('', self.port), FakeGrackleRequestHandler)
234
 
        service.store = GrackleStore(self.messages)
235
 
        for archive_id, messages in service.store.messages.iteritems():
236
 
            for message in messages:
237
 
                message.setdefault('headers', {})
238
 
        self.is_ready()
239
 
        if self.write_logs:
240
 
            logging.basicConfig(
241
 
                stream=sys.stderr, level=logging.INFO)
242
 
        service.serve_forever()
243
 
 
244
 
    def __exit__(self, exc_type, exc_val, traceback):
245
 
        os.kill(self.pid, SIGKILL)
246
 
 
247
 
 
248
 
SUPPORTED_DISPLAY_TYPES = set(['all', 'text-only', 'headers-only'])
249
 
 
250
 
 
251
 
SUPPORTED_ORDERS = set(
252
 
    ['date', 'author', 'subject', 'thread_newest', 'thread_oldest',
253
 
     'thread_subject'])
254
 
 
255
 
 
256
 
class FakeGrackleRequestHandler(BaseHTTPRequestHandler):
257
 
    """A request handler that forwards to server.store."""
258
 
 
259
 
    def __init__(self, *args, **kwargs):
260
 
        """Constructor.  Sets up logging."""
261
 
        self.logger = logging.getLogger('http')
262
 
        BaseHTTPRequestHandler.__init__(self, *args, **kwargs)
263
 
 
264
 
    def do_POST(self):
265
 
        """Create a message on POST."""
266
 
        message = self.rfile.read(int(self.headers['content-length']))
267
 
        if message == 'This is a message':
268
 
            self.send_response(httplib.CREATED)
269
 
            self.end_headers()
270
 
            self.wfile.close()
271
 
        else:
272
 
            self.send_error(httplib.BAD_REQUEST)
273
 
 
274
 
    def do_GET(self):
275
 
        """Retrieve a list of messages on GET."""
276
 
        scheme, netloc, path, params, query_string, fragments = (
277
 
            urlparse(self.path))
278
 
        parts = path.split('/')
279
 
        if parts[1] == 'archive':
280
 
            try:
281
 
                response = self.server.store.get_messages(
282
 
                    parts[2], query_string)
283
 
                self.send_response(httplib.OK)
284
 
                self.end_headers()
285
 
                self.wfile.write(simplejson.dumps(response))
286
 
            except Exception, error:
287
 
                self.send_response(
288
 
                    httplib.BAD_REQUEST, error.__doc__)
289
 
                return
290
 
 
291
 
    def log_message(self, format, *args):
292
 
        """Override log_message to use standard Python logging."""
293
 
        message = "%s - - [%s] %s\n" % (
294
 
            self.address_string(), self.log_date_time_string(), format % args)
295
 
        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')
296
69
 
297
70
 
298
71
class TestPutMessage(TestCase):
299
72
 
300
73
    def test_put_message(self):
301
 
        client = GrackleClient('localhost', 8436)
302
 
        with ForkedFakeService.from_client(client):
303
 
            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):
304
87
            with ExpectedException(Exception, 'wtf'):
305
 
                client.put_message('arch1', 'asdf',
306
 
                    StringIO('This is not a message'))
 
88
                client.put_message('no-archive', 'id1', StringIO('message'))
307
89
 
308
90
 
309
91
class TestGetMessages(TestCase):
316
98
            sorted(ids), sorted(messages, key=lambda m: m['message_id']))
317
99
 
318
100
    def test_get_messages(self):
319
 
        client = GrackleClient('localhost', 8435)
 
101
        client = GrackleClient('localhost', 8430)
320
102
        archive = {
321
103
            'baz': [make_message('foo'), make_message('bar')]}
322
104
        with ForkedFakeService.from_client(client, archive):
350
132
 
351
133
    def get_messages_member_order_test(self, key):
352
134
        client = GrackleClient('localhost', 8439)
 
135
        if key == 'author':
 
136
            header_name = 'from'
 
137
        else:
 
138
            header_name = key
353
139
        archive = {
354
140
            'baz': [
355
 
                make_message('foo', headers={key: '2011-03-25'}),
356
 
                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'}),
357
143
             ]}
358
144
        with ForkedFakeService.from_client(client, archive):
359
145
            response = client.get_messages('baz')
506
292
        first_message = response['messages'][0]
507
293
        self.assertEqual('foo', first_message['message_id'])
508
294
        self.assertEqual(
509
 
            {'From': 'me', 'Message-Id': 'foo', 'To': 'you'},
510
 
            first_message['headers'])
 
295
            archive['baz'][0]['headers'], first_message['headers'])
511
296
        self.assertNotIn('body', first_message)
512
297
 
513
298
    def test_display_type_text_only(self):
524
309
        self.assertEqual('foo', first_message['message_id'])
525
310
        self.assertEqual('me', first_message['headers']['From'])
526
311
        self.assertEqual('you', first_message['headers']['To'])
527
 
        self.assertEqual('abcdefghi', first_message['body'])
 
312
        self.assertEqual(archive['baz'][0]['body'], first_message['body'])
528
313
 
529
314
    def test_display_type_all(self):
530
315
        client = GrackleClient('localhost', 8447)
540
325
        self.assertEqual('foo', first_message['message_id'])
541
326
        self.assertEqual('me', first_message['headers']['From'])
542
327
        self.assertEqual('you', first_message['headers']['To'])
543
 
        self.assertEqual(
544
 
            'abcdefghi\n\nattactment data.', first_message['body'])
 
328
        self.assertEqual(archive['baz'][0]['body'], first_message['body'])
545
329
 
546
330
    def test_date_range(self):
547
331
        client = GrackleClient('localhost', 8448)
584
368
        with ForkedFakeService.from_client(client, archive):
585
369
            with ExpectedException(UnparsableDateRange, ''):
586
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'])