~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:49:46 UTC
  • Revision ID: curtis.hovey@canonical.com-20120214224946-rx83gm3er2pho566
Raise UnparsableDateRange when the date cannot be parsed.

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