~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:
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
 
23
24
    UnsupportedDisplayType,
24
25
    UnsupportedOrder,
25
26
    )
26
 
from grackle.store import (
27
 
    make_json_message,
28
 
    MemoryStore,
29
 
    )
30
27
 
31
28
 
32
29
def make_message(message_id, body='body', headers=None, hidden=False):
52
49
                      attachment_type=None):
53
50
    message = MIMEMultipart()
54
51
    message.attach(MIMEText(body))
55
 
    if headers is None:
56
 
        headers = {}
57
 
    for key, value in headers.items():
58
 
        message[key] = value
59
52
    if attachment_type is not None:
60
53
        attachment = Message()
61
54
        attachment.set_payload('attactment data.')
62
55
        attachment['Content-Type'] = attachment_type
63
56
        attachment['Content-Disposition'] = 'attachment; filename="file.ext"'
64
57
        message.attach(attachment)
65
 
    return make_json_message(message_id, message.as_string())
 
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
66
181
 
67
182
 
68
183
class ForkedFakeService:
69
184
    """A Grackle service fake, as a ContextManager."""
70
185
 
71
 
    def __init__(self, port, message_archives=None, write_logs=False):
 
186
    def __init__(self, port, messages=None, write_logs=False):
72
187
        """Constructor.
73
188
 
74
189
        :param port: The tcp port to use.
75
 
        :param message_archives: A dict of lists of dicts representing
76
 
            archives of messages. The outer dict represents the archive,
77
 
            the list represents the list of messages for that archive.
 
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.
78
193
        :param write_logs: If true, log messages will be written to stdout.
79
194
        """
80
195
        self.pid = None
81
196
        self.port = port
82
 
        if message_archives is None:
83
 
            self.message_archives = {}
 
197
        if messages is None:
 
198
            self.messages = {}
84
199
        else:
85
 
            self.message_archives = message_archives
 
200
            self.messages = messages
86
201
        self.read_end, self.write_end = os.pipe()
87
202
        self.write_logs = write_logs
88
203
 
89
204
    @staticmethod
90
 
    def from_client(client, message_archives=None):
 
205
    def from_client(client, messages=None):
91
206
        """Instantiate a ForkedFakeService from the client.
92
207
 
93
208
        :param port: The client to provide service for.
94
 
        :param message_archives: A dict of lists of dicts representing
95
 
            archives of messages. The outer dict represents the archive,
96
 
            the list represents the list of messages for that archive.
 
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.
97
212
        """
98
 
        return ForkedFakeService(client.port, message_archives)
 
213
        return ForkedFakeService(client.port, messages)
99
214
 
100
215
    def is_ready(self):
101
216
        """Tell the parent process that the server is ready for writes."""
116
231
    def start_server(self):
117
232
        """Start the HTTP server."""
118
233
        service = HTTPServer(('', self.port), FakeGrackleRequestHandler)
119
 
        service.store = MemoryStore(self.message_archives)
120
 
        for archive_id, messages in service.store.message_archives.iteritems():
 
234
        service.store = GrackleStore(self.messages)
 
235
        for archive_id, messages in service.store.messages.iteritems():
121
236
            for message in messages:
122
237
                message.setdefault('headers', {})
123
238
        self.is_ready()
130
245
        os.kill(self.pid, SIGKILL)
131
246
 
132
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
 
133
256
class FakeGrackleRequestHandler(BaseHTTPRequestHandler):
134
257
    """A request handler that forwards to server.store."""
135
258
 
141
264
    def do_POST(self):
142
265
        """Create a message on POST."""
143
266
        message = self.rfile.read(int(self.headers['content-length']))
144
 
        scheme, netloc, path, params, query_string, fragments = (
145
 
            urlparse(self.path))
146
 
        parts = path.split('/')
147
 
        if parts[1] == 'archive' and len(parts) == 4:
148
 
            try:
149
 
                # This expected path is /archive/archive_id/message_id.
150
 
                self.server.store.put_message(parts[2], parts[3], message)
151
 
                self.send_response(httplib.CREATED)
152
 
                self.end_headers()
153
 
                self.wfile.close()
154
 
            except:
155
 
                self.send_error(httplib.BAD_REQUEST)
 
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
273
 
157
274
    def do_GET(self):
158
275
        """Retrieve a list of messages on GET."""
181
298
class TestPutMessage(TestCase):
182
299
 
183
300
    def test_put_message(self):
184
 
        client = GrackleClient('localhost', 8420)
185
 
        message_archives = {'arch1': []}
186
 
        with ForkedFakeService.from_client(client, message_archives):
187
 
            client.put_message('arch1', 'id1', StringIO('This is a message'))
188
 
            response = client.get_messages('arch1')
189
 
        self.assertEqual(1, len(response['messages']))
190
 
        message = response['messages'][0]
191
 
        self.assertEqual('id1', message['message_id'])
192
 
 
193
 
    def test_put_message_without_archive(self):
194
 
        client = GrackleClient('localhost', 8421)
195
 
        message_archives = {'arch1': []}
196
 
        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'))
197
304
            with ExpectedException(Exception, 'wtf'):
198
 
                client.put_message('no-archive', 'id1', StringIO('message'))
 
305
                client.put_message('arch1', 'asdf',
 
306
                    StringIO('This is not a message'))
199
307
 
200
308
 
201
309
class TestGetMessages(TestCase):
208
316
            sorted(ids), sorted(messages, key=lambda m: m['message_id']))
209
317
 
210
318
    def test_get_messages(self):
211
 
        client = GrackleClient('localhost', 8430)
 
319
        client = GrackleClient('localhost', 8435)
212
320
        archive = {
213
321
            'baz': [make_message('foo'), make_message('bar')]}
214
322
        with ForkedFakeService.from_client(client, archive):
416
524
        self.assertEqual('foo', first_message['message_id'])
417
525
        self.assertEqual('me', first_message['headers']['From'])
418
526
        self.assertEqual('you', first_message['headers']['To'])
419
 
        self.assertEqual(archive['baz'][0]['body'], first_message['body'])
 
527
        self.assertEqual('abcdefghi', first_message['body'])
420
528
 
421
529
    def test_display_type_all(self):
422
530
        client = GrackleClient('localhost', 8447)
432
540
        self.assertEqual('foo', first_message['message_id'])
433
541
        self.assertEqual('me', first_message['headers']['From'])
434
542
        self.assertEqual('you', first_message['headers']['To'])
435
 
        self.assertEqual(archive['baz'][0]['body'], first_message['body'])
 
543
        self.assertEqual(
 
544
            'abcdefghi\n\nattactment data.', first_message['body'])
436
545
 
437
546
    def test_date_range(self):
438
547
        client = GrackleClient('localhost', 8448)