~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 15:01:18 UTC
  • Revision ID: curtis.hovey@canonical.com-20120316150118-jqk101q03432af8x
Use a real rfc822 message for mime testing.

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):
49
52
                      attachment_type=None):
50
53
    message = MIMEMultipart()
51
54
    message.attach(MIMEText(body))
 
55
    if headers is None:
 
56
        headers = {}
 
57
    for key, value in headers.items():
 
58
        message[key] = value
52
59
    if attachment_type is not None:
53
60
        attachment = Message()
54
61
        attachment.set_payload('attactment data.')
55
62
        attachment['Content-Type'] = attachment_type
56
63
        attachment['Content-Disposition'] = 'attachment; filename="file.ext"'
57
64
        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
 
65
    return make_json_message(message_id, message.as_string())
181
66
 
182
67
 
183
68
class ForkedFakeService:
184
69
    """A Grackle service fake, as a ContextManager."""
185
70
 
186
 
    def __init__(self, port, messages=None, write_logs=False):
 
71
    def __init__(self, port, message_archives=None, write_logs=False):
187
72
        """Constructor.
188
73
 
189
74
        :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.
 
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.
193
78
        :param write_logs: If true, log messages will be written to stdout.
194
79
        """
195
80
        self.pid = None
196
81
        self.port = port
197
 
        if messages is None:
198
 
            self.messages = {}
 
82
        if message_archives is None:
 
83
            self.message_archives = {}
199
84
        else:
200
 
            self.messages = messages
 
85
            self.message_archives = message_archives
201
86
        self.read_end, self.write_end = os.pipe()
202
87
        self.write_logs = write_logs
203
88
 
204
89
    @staticmethod
205
 
    def from_client(client, messages=None):
 
90
    def from_client(client, message_archives=None):
206
91
        """Instantiate a ForkedFakeService from the client.
207
92
 
208
93
        :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.
 
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.
212
97
        """
213
 
        return ForkedFakeService(client.port, messages)
 
98
        return ForkedFakeService(client.port, message_archives)
214
99
 
215
100
    def is_ready(self):
216
101
        """Tell the parent process that the server is ready for writes."""
231
116
    def start_server(self):
232
117
        """Start the HTTP server."""
233
118
        service = HTTPServer(('', self.port), FakeGrackleRequestHandler)
234
 
        service.store = GrackleStore(self.messages)
235
 
        for archive_id, messages in service.store.messages.iteritems():
 
119
        service.store = MemoryStore(self.message_archives)
 
120
        for archive_id, messages in service.store.message_archives.iteritems():
236
121
            for message in messages:
237
122
                message.setdefault('headers', {})
238
123
        self.is_ready()
245
130
        os.kill(self.pid, SIGKILL)
246
131
 
247
132
 
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
133
class FakeGrackleRequestHandler(BaseHTTPRequestHandler):
257
134
    """A request handler that forwards to server.store."""
258
135
 
264
141
    def do_POST(self):
265
142
        """Create a message on POST."""
266
143
        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)
 
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)
273
156
 
274
157
    def do_GET(self):
275
158
        """Retrieve a list of messages on GET."""
298
181
class TestPutMessage(TestCase):
299
182
 
300
183
    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'))
 
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):
304
197
            with ExpectedException(Exception, 'wtf'):
305
 
                client.put_message('arch1', 'asdf',
306
 
                    StringIO('This is not a message'))
 
198
                client.put_message('no-archive', 'id1', StringIO('message'))
307
199
 
308
200
 
309
201
class TestGetMessages(TestCase):
316
208
            sorted(ids), sorted(messages, key=lambda m: m['message_id']))
317
209
 
318
210
    def test_get_messages(self):
319
 
        client = GrackleClient('localhost', 8435)
 
211
        client = GrackleClient('localhost', 8430)
320
212
        archive = {
321
213
            'baz': [make_message('foo'), make_message('bar')]}
322
214
        with ForkedFakeService.from_client(client, archive):
524
416
        self.assertEqual('foo', first_message['message_id'])
525
417
        self.assertEqual('me', first_message['headers']['From'])
526
418
        self.assertEqual('you', first_message['headers']['To'])
527
 
        self.assertEqual('abcdefghi', first_message['body'])
 
419
        self.assertEqual(archive['baz'][0]['body'], first_message['body'])
528
420
 
529
421
    def test_display_type_all(self):
530
422
        client = GrackleClient('localhost', 8447)
540
432
        self.assertEqual('foo', first_message['message_id'])
541
433
        self.assertEqual('me', first_message['headers']['From'])
542
434
        self.assertEqual('you', first_message['headers']['To'])
543
 
        self.assertEqual(
544
 
            'abcdefghi\n\nattactment data.', first_message['body'])
 
435
        self.assertEqual(archive['baz'][0]['body'], first_message['body'])
545
436
 
546
437
    def test_date_range(self):
547
438
        client = GrackleClient('localhost', 8448)