~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:55:57 UTC
  • Revision ID: curtis.hovey@canonical.com-20120214225557-pi20aqjex7wtiwpl
Added test for extra data argument.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
__metaclass__ = type
2
 
 
 
1
from BaseHTTPServer import (
 
2
    HTTPServer,
 
3
    BaseHTTPRequestHandler,
 
4
    )
3
5
from email.message import Message
4
6
from email.mime.multipart import MIMEMultipart
5
7
from email.mime.text import MIMEText
 
8
import httplib
 
9
import logging
 
10
import os
 
11
from signal import SIGKILL
 
12
import simplejson
6
13
from StringIO import StringIO
 
14
import sys
7
15
from unittest import TestCase
 
16
from urlparse import urlparse
 
17
from urlparse import parse_qs
8
18
 
9
19
from testtools import ExpectedException
10
20
 
11
 
from grackle.client import GrackleClient
12
 
from grackle.error import (
13
 
    ArchiveIdExists,
 
21
from grackle.client import (
 
22
    GrackleClient,
14
23
    UnparsableDateRange,
15
24
    UnsupportedDisplayType,
16
25
    UnsupportedOrder,
17
26
    )
18
 
from grackle.service import ForkedFakeService
19
 
from grackle.store import make_json_message
20
27
 
21
28
 
22
29
def make_message(message_id, body='body', headers=None, hidden=False):
23
30
    if headers is None:
24
31
        headers = {}
25
 
    message_headers = {
26
 
        'Message-Id': message_id,
27
 
        'date': '2005-01-01',
28
 
        'subject': 'subject',
29
 
        'from': 'author',
30
 
        '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,
31
44
        }
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)
 
45
    return message
38
46
 
39
47
 
40
48
def make_mime_message(message_id, body='body', headers=None, hidden=False,
41
49
                      attachment_type=None):
42
 
    parts = MIMEMultipart()
43
 
    parts.attach(MIMEText(body))
 
50
    message = MIMEMultipart()
 
51
    message.attach(MIMEText(body))
44
52
    if attachment_type is not None:
45
53
        attachment = Message()
46
54
        attachment.set_payload('attactment data.')
47
55
        attachment['Content-Type'] = attachment_type
48
56
        attachment['Content-Disposition'] = 'attachment; filename="file.ext"'
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')
 
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)
69
296
 
70
297
 
71
298
class TestPutMessage(TestCase):
72
299
 
73
300
    def test_put_message(self):
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):
 
301
        client = GrackleClient('localhost', 8436)
 
302
        with ForkedFakeService.from_client(client):
 
303
            client.put_message('arch1', 'asdf', StringIO('This is a message'))
87
304
            with ExpectedException(Exception, 'wtf'):
88
 
                client.put_message('no-archive', 'id1', StringIO('message'))
 
305
                client.put_message('arch1', 'asdf',
 
306
                    StringIO('This is not a message'))
89
307
 
90
308
 
91
309
class TestGetMessages(TestCase):
98
316
            sorted(ids), sorted(messages, key=lambda m: m['message_id']))
99
317
 
100
318
    def test_get_messages(self):
101
 
        client = GrackleClient('localhost', 8430)
 
319
        client = GrackleClient('localhost', 8435)
102
320
        archive = {
103
321
            'baz': [make_message('foo'), make_message('bar')]}
104
322
        with ForkedFakeService.from_client(client, archive):
132
350
 
133
351
    def get_messages_member_order_test(self, key):
134
352
        client = GrackleClient('localhost', 8439)
135
 
        if key == 'author':
136
 
            header_name = 'from'
137
 
        else:
138
 
            header_name = key
139
353
        archive = {
140
354
            'baz': [
141
 
                make_message('foo', headers={header_name: '2011-03-25'}),
142
 
                make_message('bar', headers={header_name: '2011-03-24'}),
 
355
                make_message('foo', headers={key: '2011-03-25'}),
 
356
                make_message('bar', headers={key: '2011-03-24'}),
143
357
             ]}
144
358
        with ForkedFakeService.from_client(client, archive):
145
359
            response = client.get_messages('baz')
292
506
        first_message = response['messages'][0]
293
507
        self.assertEqual('foo', first_message['message_id'])
294
508
        self.assertEqual(
295
 
            archive['baz'][0]['headers'], first_message['headers'])
 
509
            {'From': 'me', 'Message-Id': 'foo', 'To': 'you'},
 
510
            first_message['headers'])
296
511
        self.assertNotIn('body', first_message)
297
512
 
298
513
    def test_display_type_text_only(self):
309
524
        self.assertEqual('foo', first_message['message_id'])
310
525
        self.assertEqual('me', first_message['headers']['From'])
311
526
        self.assertEqual('you', first_message['headers']['To'])
312
 
        self.assertEqual(archive['baz'][0]['body'], first_message['body'])
 
527
        self.assertEqual('abcdefghi', first_message['body'])
313
528
 
314
529
    def test_display_type_all(self):
315
530
        client = GrackleClient('localhost', 8447)
325
540
        self.assertEqual('foo', first_message['message_id'])
326
541
        self.assertEqual('me', first_message['headers']['From'])
327
542
        self.assertEqual('you', first_message['headers']['To'])
328
 
        self.assertEqual(archive['baz'][0]['body'], first_message['body'])
 
543
        self.assertEqual(
 
544
            'abcdefghi\n\nattactment data.', first_message['body'])
329
545
 
330
546
    def test_date_range(self):
331
547
        client = GrackleClient('localhost', 8448)
368
584
        with ForkedFakeService.from_client(client, archive):
369
585
            with ExpectedException(UnparsableDateRange, ''):
370
586
                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'])