~didrocks/unity/altf10

« back to all changes in this revision

Viewing changes to grackle/tests/test_client.py

  • Committer: Curtis Hovey
  • Date: 2012-03-16 19:49:31 UTC
  • Revision ID: curtis.hovey@canonical.com-20120316194931-xngt1fdw9ewovwqn
Use PUT for creating messages.

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
18
17
 
19
18
from testtools import ExpectedException
20
19
 
24
23
    UnsupportedDisplayType,
25
24
    UnsupportedOrder,
26
25
    )
 
26
from grackle.store import (
 
27
    make_json_message,
 
28
    MemoryStore,
 
29
    )
27
30
 
28
31
 
29
32
def make_message(message_id, body='body', headers=None, hidden=False):
30
33
    if headers is None:
31
34
        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,
 
35
    message_headers = {
 
36
        'Message-Id': message_id,
 
37
        'date': '2005-01-01',
 
38
        'subject': 'subject',
 
39
        'from': 'author',
 
40
        'replies': '',
44
41
        }
45
 
    return message
 
42
    message_headers.update(headers.items())
 
43
    message = Message()
 
44
    message.set_payload(body)
 
45
    for key, value in message_headers.items():
 
46
        message[key] = value
 
47
    return make_json_message(message_id, message.as_string(), hidden)
46
48
 
47
49
 
48
50
def make_mime_message(message_id, body='body', headers=None, hidden=False,
49
51
                      attachment_type=None):
50
 
    message = MIMEMultipart()
51
 
    message.attach(MIMEText(body))
 
52
    parts = MIMEMultipart()
 
53
    parts.attach(MIMEText(body))
52
54
    if attachment_type is not None:
53
55
        attachment = Message()
54
56
        attachment.set_payload('attactment data.')
55
57
        attachment['Content-Type'] = attachment_type
56
58
        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
 
59
        parts.attach(attachment)
 
60
    return make_message(message_id, parts.as_string(), headers, hidden)
181
61
 
182
62
 
183
63
class ForkedFakeService:
184
64
    """A Grackle service fake, as a ContextManager."""
185
65
 
186
 
    def __init__(self, port, messages=None, write_logs=False):
 
66
    def __init__(self, port, message_archives=None, write_logs=False):
187
67
        """Constructor.
188
68
 
189
69
        :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.
 
70
        :param message_archives: A dict of lists of dicts representing
 
71
            archives of messages. The outer dict represents the archive,
 
72
            the list represents the list of messages for that archive.
193
73
        :param write_logs: If true, log messages will be written to stdout.
194
74
        """
195
75
        self.pid = None
196
76
        self.port = port
197
 
        if messages is None:
198
 
            self.messages = {}
 
77
        if message_archives is None:
 
78
            self.message_archives = {}
199
79
        else:
200
 
            self.messages = messages
 
80
            self.message_archives = message_archives
201
81
        self.read_end, self.write_end = os.pipe()
202
82
        self.write_logs = write_logs
203
83
 
204
84
    @staticmethod
205
 
    def from_client(client, messages=None):
 
85
    def from_client(client, message_archives=None):
206
86
        """Instantiate a ForkedFakeService from the client.
207
87
 
208
88
        :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.
 
89
        :param message_archives: A dict of lists of dicts representing
 
90
            archives of messages. The outer dict represents the archive,
 
91
            the list represents the list of messages for that archive.
212
92
        """
213
 
        return ForkedFakeService(client.port, messages)
 
93
        return ForkedFakeService(client.port, message_archives)
214
94
 
215
95
    def is_ready(self):
216
96
        """Tell the parent process that the server is ready for writes."""
231
111
    def start_server(self):
232
112
        """Start the HTTP server."""
233
113
        service = HTTPServer(('', self.port), FakeGrackleRequestHandler)
234
 
        service.store = GrackleStore(self.messages)
235
 
        for archive_id, messages in service.store.messages.iteritems():
 
114
        service.store = MemoryStore(self.message_archives)
 
115
        for archive_id, messages in service.store.message_archives.iteritems():
236
116
            for message in messages:
237
117
                message.setdefault('headers', {})
238
118
        self.is_ready()
245
125
        os.kill(self.pid, SIGKILL)
246
126
 
247
127
 
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
128
class FakeGrackleRequestHandler(BaseHTTPRequestHandler):
257
129
    """A request handler that forwards to server.store."""
258
130
 
261
133
        self.logger = logging.getLogger('http')
262
134
        BaseHTTPRequestHandler.__init__(self, *args, **kwargs)
263
135
 
 
136
    def do_PUT(self):
 
137
        """Create an archive or message on PUT."""
 
138
        scheme, netloc, path, params, query_string, fragments = (
 
139
            urlparse(self.path))
 
140
        parts = path.split('/')
 
141
        if parts[1] != 'archive':
 
142
            # This is an unknonwn operation?
 
143
            return
 
144
        if len(parts) == 4:
 
145
            # This expected path is /archive/archive_id/message_id.
 
146
            try:
 
147
                message = self.rfile.read(int(self.headers['content-length']))
 
148
                self.server.store.put_message(parts[2], parts[3], message)
 
149
                self.send_response(httplib.CREATED)
 
150
                self.end_headers()
 
151
                self.wfile.close()
 
152
            except:
 
153
                self.send_error(httplib.BAD_REQUEST)
 
154
 
264
155
    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)
 
156
        """Change a message on POST."""
 
157
        scheme, netloc, path, params, query_string, fragments = (
 
158
            urlparse(self.path))
 
159
        parts = path.split('/')
 
160
        if parts[1] != 'archive':
 
161
            # This is an unknonwn operation?
 
162
            return
 
163
        if len(parts) == 4:
 
164
            # This expected path is /archive/archive_id/message_id.
 
165
            try:
 
166
                # This expected path is /archive/archive_id/message_id.
 
167
                response = self.server.store.hide_message(
 
168
                    parts[2], parts[3], query_string)
 
169
                self.send_response(httplib.OK)
 
170
                self.end_headers()
 
171
                self.wfile.write(simplejson.dumps(response))
 
172
            except:
 
173
                self.send_error(httplib.BAD_REQUEST)
273
174
 
274
175
    def do_GET(self):
275
176
        """Retrieve a list of messages on GET."""
298
199
class TestPutMessage(TestCase):
299
200
 
300
201
    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'))
 
202
        client = GrackleClient('localhost', 8420)
 
203
        message_archives = {'arch1': []}
 
204
        with ForkedFakeService.from_client(client, message_archives):
 
205
            client.put_message('arch1', 'id1', StringIO('This is a message'))
 
206
            response = client.get_messages('arch1')
 
207
        self.assertEqual(1, len(response['messages']))
 
208
        message = response['messages'][0]
 
209
        self.assertEqual('id1', message['message_id'])
 
210
 
 
211
    def test_put_message_without_archive(self):
 
212
        client = GrackleClient('localhost', 8421)
 
213
        message_archives = {'arch1': []}
 
214
        with ForkedFakeService.from_client(client, message_archives):
304
215
            with ExpectedException(Exception, 'wtf'):
305
 
                client.put_message('arch1', 'asdf',
306
 
                    StringIO('This is not a message'))
 
216
                client.put_message('no-archive', 'id1', StringIO('message'))
307
217
 
308
218
 
309
219
class TestGetMessages(TestCase):
316
226
            sorted(ids), sorted(messages, key=lambda m: m['message_id']))
317
227
 
318
228
    def test_get_messages(self):
319
 
        client = GrackleClient('localhost', 8435)
 
229
        client = GrackleClient('localhost', 8430)
320
230
        archive = {
321
231
            'baz': [make_message('foo'), make_message('bar')]}
322
232
        with ForkedFakeService.from_client(client, archive):
350
260
 
351
261
    def get_messages_member_order_test(self, key):
352
262
        client = GrackleClient('localhost', 8439)
 
263
        if key == 'author':
 
264
            header_name = 'from'
 
265
        else:
 
266
            header_name = key
353
267
        archive = {
354
268
            'baz': [
355
 
                make_message('foo', headers={key: '2011-03-25'}),
356
 
                make_message('bar', headers={key: '2011-03-24'}),
 
269
                make_message('foo', headers={header_name: '2011-03-25'}),
 
270
                make_message('bar', headers={header_name: '2011-03-24'}),
357
271
             ]}
358
272
        with ForkedFakeService.from_client(client, archive):
359
273
            response = client.get_messages('baz')
506
420
        first_message = response['messages'][0]
507
421
        self.assertEqual('foo', first_message['message_id'])
508
422
        self.assertEqual(
509
 
            {'From': 'me', 'Message-Id': 'foo', 'To': 'you'},
510
 
            first_message['headers'])
 
423
            archive['baz'][0]['headers'], first_message['headers'])
511
424
        self.assertNotIn('body', first_message)
512
425
 
513
426
    def test_display_type_text_only(self):
524
437
        self.assertEqual('foo', first_message['message_id'])
525
438
        self.assertEqual('me', first_message['headers']['From'])
526
439
        self.assertEqual('you', first_message['headers']['To'])
527
 
        self.assertEqual('abcdefghi', first_message['body'])
 
440
        self.assertEqual(archive['baz'][0]['body'], first_message['body'])
528
441
 
529
442
    def test_display_type_all(self):
530
443
        client = GrackleClient('localhost', 8447)
540
453
        self.assertEqual('foo', first_message['message_id'])
541
454
        self.assertEqual('me', first_message['headers']['From'])
542
455
        self.assertEqual('you', first_message['headers']['To'])
543
 
        self.assertEqual(
544
 
            'abcdefghi\n\nattactment data.', first_message['body'])
 
456
        self.assertEqual(archive['baz'][0]['body'], first_message['body'])
545
457
 
546
458
    def test_date_range(self):
547
459
        client = GrackleClient('localhost', 8448)
584
496
        with ForkedFakeService.from_client(client, archive):
585
497
            with ExpectedException(UnparsableDateRange, ''):
586
498
                client.get_messages('baz', date_range='2012-01..12-02..12-03')
 
499
 
 
500
 
 
501
class TestHideMessages(TestCase):
 
502
 
 
503
    def test_hide_message_true(self):
 
504
        client = GrackleClient('localhost', 8470)
 
505
        archive = {
 
506
            'baz': [
 
507
                make_message('foo', hidden=False),
 
508
            ]}
 
509
        with ForkedFakeService.from_client(client, archive):
 
510
            response = client.hide_message('baz', 'foo', hidden=True)
 
511
        self.assertEqual('foo', response['message_id'])
 
512
        self.assertIs(True, response['hidden'])
 
513
 
 
514
    def test_hide_message_false(self):
 
515
        client = GrackleClient('localhost', 8470)
 
516
        archive = {
 
517
            'baz': [
 
518
                make_message('foo', hidden=True),
 
519
            ]}
 
520
        with ForkedFakeService.from_client(client, archive):
 
521
            response = client.hide_message('baz', 'foo', hidden=False)
 
522
        self.assertEqual('foo', response['message_id'])
 
523
        self.assertIs(False, response['hidden'])