~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:52:55 UTC
  • Revision ID: curtis.hovey@canonical.com-20120214225255-7cwc33qlpmaztebo
Dates must be a value.

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
 
    MemoryStore,
28
 
    )
29
27
 
30
28
 
31
29
def make_message(message_id, body='body', headers=None, hidden=False):
60
58
    return make_message(message_id, message.get_payload(), headers, hidden)
61
59
 
62
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
 
63
183
class ForkedFakeService:
64
184
    """A Grackle service fake, as a ContextManager."""
65
185
 
66
 
    def __init__(self, port, message_archives=None, write_logs=False):
 
186
    def __init__(self, port, messages=None, write_logs=False):
67
187
        """Constructor.
68
188
 
69
189
        :param port: The tcp port to use.
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.
 
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.
73
193
        :param write_logs: If true, log messages will be written to stdout.
74
194
        """
75
195
        self.pid = None
76
196
        self.port = port
77
 
        if message_archives is None:
78
 
            self.message_archives = {}
 
197
        if messages is None:
 
198
            self.messages = {}
79
199
        else:
80
 
            self.message_archives = message_archives
 
200
            self.messages = messages
81
201
        self.read_end, self.write_end = os.pipe()
82
202
        self.write_logs = write_logs
83
203
 
84
204
    @staticmethod
85
 
    def from_client(client, message_archives=None):
 
205
    def from_client(client, messages=None):
86
206
        """Instantiate a ForkedFakeService from the client.
87
207
 
88
208
        :param port: The client to provide service for.
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.
 
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.
92
212
        """
93
 
        return ForkedFakeService(client.port, message_archives)
 
213
        return ForkedFakeService(client.port, messages)
94
214
 
95
215
    def is_ready(self):
96
216
        """Tell the parent process that the server is ready for writes."""
111
231
    def start_server(self):
112
232
        """Start the HTTP server."""
113
233
        service = HTTPServer(('', self.port), FakeGrackleRequestHandler)
114
 
        service.store = MemoryStore(self.message_archives)
115
 
        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():
116
236
            for message in messages:
117
237
                message.setdefault('headers', {})
118
238
        self.is_ready()
125
245
        os.kill(self.pid, SIGKILL)
126
246
 
127
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
 
128
256
class FakeGrackleRequestHandler(BaseHTTPRequestHandler):
129
257
    """A request handler that forwards to server.store."""
130
258
 
170
298
class TestPutMessage(TestCase):
171
299
 
172
300
    def test_put_message(self):
173
 
        client = GrackleClient('localhost', 8420)
 
301
        client = GrackleClient('localhost', 8436)
174
302
        with ForkedFakeService.from_client(client):
175
303
            client.put_message('arch1', 'asdf', StringIO('This is a message'))
176
304
            with ExpectedException(Exception, 'wtf'):
188
316
            sorted(ids), sorted(messages, key=lambda m: m['message_id']))
189
317
 
190
318
    def test_get_messages(self):
191
 
        client = GrackleClient('localhost', 8430)
 
319
        client = GrackleClient('localhost', 8435)
192
320
        archive = {
193
321
            'baz': [make_message('foo'), make_message('bar')]}
194
322
        with ForkedFakeService.from_client(client, archive):
437
565
        self.assertEqual(['bar', 'naf', 'qux'], ids)
438
566
 
439
567
    def test_date_range_unparsabledaterange(self):
440
 
        client = GrackleClient('localhost', 8449)
 
568
        client = GrackleClient('localhost', 8448)
441
569
        archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
442
570
        with ForkedFakeService.from_client(client, archive):
443
571
            with ExpectedException(UnparsableDateRange, ''):
444
572
                client.get_messages('baz', date_range='2012-01-01')
445
573
 
446
574
    def test_date_range_unparsabledaterange_missing_part(self):
447
 
        client = GrackleClient('localhost', 8450)
 
575
        client = GrackleClient('localhost', 8448)
448
576
        archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
449
577
        with ForkedFakeService.from_client(client, archive):
450
578
            with ExpectedException(UnparsableDateRange, ''):
451
579
                client.get_messages('baz', date_range='2012-01-01..')
452
 
 
453
 
    def test_date_range_unparsabledaterange_extra_part(self):
454
 
        client = GrackleClient('localhost', 8451)
455
 
        archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
456
 
        with ForkedFakeService.from_client(client, archive):
457
 
            with ExpectedException(UnparsableDateRange, ''):
458
 
                client.get_messages('baz', date_range='2012-01..12-02..12-03')