Merge lp:~txconvore-maint/txconvore/initial-tests into lp:txconvore
- initial-tests
- Merge into trunk
Proposed by
Jonathan Jacobs
Status: | Merged | ||||
---|---|---|---|---|---|
Merged at revision: | 2 | ||||
Proposed branch: | lp:~txconvore-maint/txconvore/initial-tests | ||||
Merge into: | lp:txconvore | ||||
Diff against target: |
1867 lines (+1591/-73) 22 files modified
.bzrignore (+1/-0) txconvore/client.py (+157/-73) txconvore/test/data/account_groups.json (+64/-0) txconvore/test/data/account_groups_create.json (+18/-0) txconvore/test/data/account_mentions.json (+51/-0) txconvore/test/data/account_online.json (+15/-0) txconvore/test/data/account_verify.json (+6/-0) txconvore/test/data/groups.json (+18/-0) txconvore/test/data/groups_members.json (+22/-0) txconvore/test/data/groups_online.json (+25/-0) txconvore/test/data/groups_topics.json (+37/-0) txconvore/test/data/groups_topics_create.json (+16/-0) txconvore/test/data/messages_star.json (+11/-0) txconvore/test/data/messages_unstar.json (+11/-0) txconvore/test/data/topics_edit.json (+16/-0) txconvore/test/data/topics_info.json (+16/-0) txconvore/test/data/topics_messages.json (+45/-0) txconvore/test/data/topics_messages_create.json (+15/-0) txconvore/test/data/users_info.json (+12/-0) txconvore/test/test_client.py (+697/-0) txconvore/test/test_util.py (+148/-0) txconvore/util.py (+190/-0) |
||||
To merge this branch: | bzr merge lp:~txconvore-maint/txconvore/initial-tests | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
txConvore maintainers | Pending | ||
Review via email: mp+57532@code.launchpad.net |
Commit message
Description of the change
To post a comment you must log in.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === added file '.bzrignore' |
2 | --- .bzrignore 1970-01-01 00:00:00 +0000 |
3 | +++ .bzrignore 2011-04-13 16:28:00 +0000 |
4 | @@ -0,0 +1,1 @@ |
5 | +_trial_temp |
6 | |
7 | === modified file 'txconvore/client.py' |
8 | --- txconvore/client.py 2011-03-25 16:49:16 +0000 |
9 | +++ txconvore/client.py 2011-04-13 16:28:00 +0000 |
10 | @@ -1,12 +1,13 @@ |
11 | +import urllib |
12 | from base64 import b64encode |
13 | -from urllib import quote, urlencode |
14 | |
15 | try: |
16 | import json |
17 | + json # For Pyflakes. |
18 | except: |
19 | import simplejson as json |
20 | |
21 | -from twisted.web.client import getPage |
22 | +from txconvore.util import PerseverantDownloader |
23 | |
24 | |
25 | |
26 | @@ -37,6 +38,93 @@ |
27 | |
28 | |
29 | |
30 | +class Endpoint(object): |
31 | + """ |
32 | + A Convore endpoint. |
33 | + |
34 | + Used to submit requests to Convore. |
35 | + |
36 | + @type debug: C{bool} |
37 | + @cvar debug: Enable debug logging. |
38 | + |
39 | + @type baseURL: C{str} |
40 | + @ivar baseURL: Base URL of this endpoint, including the trailing slash. |
41 | + |
42 | + @type version: C{str} or C{None} |
43 | + @ivar version: API version to use, or C{None} for the latest. |
44 | + |
45 | + @ivar creds: A credentials object, or C{None} to authenticate anonymously. |
46 | + """ |
47 | + debug = False |
48 | + |
49 | + |
50 | + def __init__(self, baseURL='https://convore.com/api/', version=None, |
51 | + creds=None): |
52 | + if creds is not None and not isinstance(creds, BasicCreds): |
53 | + raise ValueError('Only basic authentication is supported') |
54 | + self.baseURL = baseURL |
55 | + self.version = version |
56 | + self.creds = creds |
57 | + |
58 | + |
59 | + def getRootURL(self): |
60 | + """ |
61 | + Get the root endpoint URL, including trailing slash. |
62 | + """ |
63 | + url = self.baseURL |
64 | + if self.version is not None: |
65 | + url += self.version + '/' |
66 | + return url |
67 | + |
68 | + |
69 | + def getPage(self, *a, **kw): |
70 | + downloader = PerseverantDownloader(*a, **kw) |
71 | + downloader.debug = self.debug |
72 | + return downloader.download() |
73 | + |
74 | + |
75 | + def submit(self, url, method, data=None, headers=None): |
76 | + """ |
77 | + Submit a request to a Convore endpoint. |
78 | + |
79 | + @type url: C{str} |
80 | + @param url: URL to submit to. |
81 | + |
82 | + @type method: C{str} |
83 | + @param method: HTTP method to use when submitting a request. |
84 | + |
85 | + @type data: C{dict} |
86 | + @param data: HTTP POST data, or C{None}. |
87 | + |
88 | + @type headers: C{dict} |
89 | + @param headers: HTTP headers to include when submitting a request. |
90 | + |
91 | + @rtype: C{Deferred} |
92 | + """ |
93 | + if headers is None: |
94 | + headers = {} |
95 | + |
96 | + headers['Accept'] = 'application/json' |
97 | + if data is not None and not isinstance(data, str): |
98 | + data = urllib.urlencode(data) |
99 | + headers['Content-Type'] = 'application/x-www-form-urlencoded' |
100 | + |
101 | + if self.creds is not None: |
102 | + headers['Authorization'] = self.creds.encode() |
103 | + |
104 | + def _parse((res, headers)): |
105 | + if res: |
106 | + return json.loads(unicode(res, 'utf-8')) |
107 | + |
108 | + d = self.getPage(url=url, |
109 | + method=method, |
110 | + postdata=data, |
111 | + headers=headers) |
112 | + d.addCallback(_parse) |
113 | + return d |
114 | + |
115 | + |
116 | + |
117 | class _ConvoreAPI(object): |
118 | collectionName = None |
119 | |
120 | @@ -45,41 +133,57 @@ |
121 | self.components = [] |
122 | |
123 | |
124 | + def _translateValue(self, value): |
125 | + if isinstance(value, bool): |
126 | + return [u'false', u'true'][value] |
127 | + elif not isinstance(value, unicode): |
128 | + return unicode(value, 'ascii') |
129 | + return value |
130 | + |
131 | + |
132 | def getURL(self, endpoint, prefix=None, suffix=[], params={}): |
133 | """ |
134 | Get the URL for this path as accessed through the given endpoint. |
135 | |
136 | @type endpoint: L{Endpoint} |
137 | - @param endpoint: The endpoint to operate through. |
138 | + @param endpoint: Endpoint to operate through. |
139 | |
140 | @type prefix: C{list} of C{unicode} |
141 | - @param prefix: A list of components to prepend before this path. |
142 | + @param prefix: List of components to prepend before this path, or |
143 | + C{None}. |
144 | |
145 | @type suffix: C{list} of C{unicode} |
146 | - @param suffix: A list of components to append after this path. |
147 | + @param suffix: List of components to append after this path, or |
148 | + C{None}. |
149 | + |
150 | + @type params: C{dict} mapping C{unicode} to C{unicode} |
151 | + @param params: Mapping of URL query parameter names and values, or |
152 | + C{None}. |
153 | |
154 | @rtype: C{str} |
155 | """ |
156 | - if self.collectionName is None: |
157 | - raise NotImplementedError( |
158 | - 'Must override collectionName on _ConvoreAPI subclasses') |
159 | - |
160 | components = [] |
161 | if prefix is None: |
162 | + if self.collectionName is None: |
163 | + raise NotImplementedError( |
164 | + 'Must override collectionName on _ConvoreAPI subclasses') |
165 | components.append(self.collectionName) |
166 | else: |
167 | components.extend(prefix) |
168 | components.extend(self.components) |
169 | components.extend(suffix) |
170 | |
171 | - def _quote(xs): |
172 | - return [quote(x.encode('utf-8'), safe='') |
173 | - for x in xs] |
174 | + def _quote(v): |
175 | + return urllib.quote( |
176 | + self._translateValue(v).encode('utf-8'), safe='') |
177 | |
178 | - components = [quote(component.encode('utf-8'), safe='') |
179 | - for component in components] |
180 | - params = [(k, v) for k, v in params.iteritems() if v is not None] |
181 | - return endpoint.getRootURL() + '/'.join(components) + '?' + urlencode(params) |
182 | + components = [_quote(component) for component in components] |
183 | + url = endpoint.getRootURL() + '/'.join(components) |
184 | + params = [(_quote(k), _quote(v)) |
185 | + for k, v in params.iteritems() if v is not None] |
186 | + if params: |
187 | + url = url + '?' + urllib.urlencode(params) |
188 | + return url |
189 | |
190 | |
191 | |
192 | @@ -143,6 +247,24 @@ |
193 | |
194 | |
195 | |
196 | +class User(_ConvoreAPI): |
197 | + collectionName = 'users' |
198 | + |
199 | + |
200 | + def __init__(self, userID): |
201 | + super(User, self).__init__() |
202 | + self.components = [userID] |
203 | + |
204 | + |
205 | + def info(self, endpoint): |
206 | + """ |
207 | + Get detailed information about the user. |
208 | + """ |
209 | + url = self.getURL(endpoint) + '.json' |
210 | + return endpoint.submit(url, 'GET') |
211 | + |
212 | + |
213 | + |
214 | class Group(_ConvoreAPI): |
215 | collectionName = 'groups' |
216 | |
217 | @@ -191,8 +313,8 @@ |
218 | """ |
219 | Mark all messages in the group as read. |
220 | """ |
221 | - url = self.geturl(endpoint, suffix=['mark_read.json']) |
222 | - return endpoint.submit(url, 'post') |
223 | + url = self.getURL(endpoint, suffix=['mark_read.json']) |
224 | + return endpoint.submit(url, 'POST') |
225 | |
226 | |
227 | def members(self, endpoint, filter=None): |
228 | @@ -250,15 +372,19 @@ |
229 | self.components = [topicID] |
230 | |
231 | |
232 | - def createMessage(self, endpoint, message): |
233 | + def createMessage(self, endpoint, message, pasted=False): |
234 | """ |
235 | Post a new message. |
236 | + |
237 | + @type pasted: C{bool} |
238 | + @param pasted: Is the message preformatted? |
239 | """ |
240 | url = self.getURL(endpoint, suffix=['messages', 'create.json']) |
241 | return endpoint.submit( |
242 | url, |
243 | 'POST', |
244 | - data=dict(message=message)) |
245 | + data=dict(message=message, |
246 | + pasted=pasted)) |
247 | |
248 | |
249 | def delete(self, endpoint): |
250 | @@ -270,6 +396,15 @@ |
251 | return endpoint.submit(url, 'POST') |
252 | |
253 | |
254 | + def edit(self, endpoint, name): |
255 | + """ |
256 | + Edit a topic. You must be the creator of the topic or a group admin in |
257 | + order to edit the topic. |
258 | + """ |
259 | + url = self.getURL(endpoint, suffix=['edit.json']) |
260 | + return endpoint.submit(url, 'POST', data=dict(name=name)) |
261 | + |
262 | + |
263 | def info(self, endpoint): |
264 | """ |
265 | Get detailed information about the topic. |
266 | @@ -282,8 +417,8 @@ |
267 | """ |
268 | Mark all messages in a topic as read. |
269 | """ |
270 | - url = self.geturl(endpoint, suffix=['mark_read.json']) |
271 | - return endpoint.submit(url, 'post') |
272 | + url = self.getURL(endpoint, suffix=['mark_read.json']) |
273 | + return endpoint.submit(url, 'POST') |
274 | |
275 | |
276 | def messages(self, endpoint, untilMessageID=None, markRead=False): |
277 | @@ -331,54 +466,3 @@ |
278 | """ |
279 | url = self.getURL(endpoint, suffix=['delete.json']) |
280 | return endpoint.submit(url, 'POST') |
281 | - |
282 | - |
283 | - |
284 | -class Endpoint(object): |
285 | - def __init__(self, baseURL='https://convore.com/api/', version=None, |
286 | - creds=None): |
287 | - if creds is not None and not isinstance(creds, BasicCreds): |
288 | - raise ValueError('Only basic authentication is supported') |
289 | - |
290 | - self.baseURL = baseURL |
291 | - self.version = version |
292 | - self.creds = creds |
293 | - |
294 | - |
295 | - def getRootURL(self): |
296 | - """ |
297 | - Get the root endpoint URL, including trailing slash. |
298 | - """ |
299 | - url = self.baseURL |
300 | - if self.version is not None: |
301 | - url += self.version + '/' |
302 | - return url |
303 | - |
304 | - |
305 | - def getPage(self, *a, **kw): |
306 | - return getPage(*a, **kw) |
307 | - |
308 | - |
309 | - def submit(self, url, method, data=None, headers=None): |
310 | - print 'Submitting %s (%s: %r)...' % (url, method, data) |
311 | - if headers is None: |
312 | - headers = {} |
313 | - |
314 | - headers['Accept'] = 'application/json' |
315 | - if data is not None and not isinstance(data, str): |
316 | - data = urlencode(data) |
317 | - headers['Content-Type'] = 'application/x-www-form-urlencoded' |
318 | - |
319 | - if self.creds is not None: |
320 | - headers['Authorization'] = self.creds.encode() |
321 | - |
322 | - def _parse(res): |
323 | - if res: |
324 | - return json.loads(unicode(res, 'utf-8')) |
325 | - |
326 | - d = self.getPage(url=url, |
327 | - method=method, |
328 | - postdata=data, |
329 | - headers=headers, |
330 | - agent='txConvore') |
331 | - return d.addCallback(_parse) |
332 | |
333 | === added directory 'txconvore/test' |
334 | === added file 'txconvore/test/__init__.py' |
335 | === added directory 'txconvore/test/data' |
336 | === added file 'txconvore/test/data/account_groups.json' |
337 | --- txconvore/test/data/account_groups.json 1970-01-01 00:00:00 +0000 |
338 | +++ txconvore/test/data/account_groups.json 2011-04-13 16:28:00 +0000 |
339 | @@ -0,0 +1,64 @@ |
340 | +{ |
341 | + "groups": [ |
342 | + { |
343 | + "creator": { |
344 | + "id": "XXX", |
345 | + "img": "XXX", |
346 | + "url": "XXX", |
347 | + "username": "fdrake" |
348 | + }, |
349 | + "date_created": 1297277511.4270909, |
350 | + "date_latest_message": 1301140974.9002681, |
351 | + "id": "XXX", |
352 | + "is_admin": false, |
353 | + "is_member": true, |
354 | + "kind": "public", |
355 | + "members_count": 2051, |
356 | + "name": "Python", |
357 | + "slug": "python", |
358 | + "topics_count": 103, |
359 | + "unread": 1, |
360 | + "url": "XXX" |
361 | + }, |
362 | + { |
363 | + "creator": { |
364 | + "id": "XXX", |
365 | + "img": "XXX", |
366 | + "url": "XXX", |
367 | + "username": "djchall" |
368 | + }, |
369 | + "date_created": 1296454650.842808, |
370 | + "date_latest_message": 1301122342.4191129, |
371 | + "id": "XXX", |
372 | + "is_admin": false, |
373 | + "is_member": true, |
374 | + "kind": "public", |
375 | + "members_count": 378, |
376 | + "name": "Feedback", |
377 | + "slug": "feedback", |
378 | + "topics_count": 446, |
379 | + "unread": 0, |
380 | + "url": "XXX" |
381 | + }, |
382 | + { |
383 | + "creator": { |
384 | + "id": "XXX", |
385 | + "img": "XXX", |
386 | + "url": "XXX", |
387 | + "username": "mithrandi" |
388 | + }, |
389 | + "date_created": 1300960771.1402569, |
390 | + "date_latest_message": 1301099446.2922111, |
391 | + "id": "XXX", |
392 | + "is_admin": false, |
393 | + "is_member": true, |
394 | + "kind": "public", |
395 | + "members_count": 13, |
396 | + "name": "Twisted", |
397 | + "slug": "twisted", |
398 | + "topics_count": 1, |
399 | + "unread": 0, |
400 | + "url": "XXX" |
401 | + } |
402 | + ] |
403 | +} |
404 | |
405 | === added file 'txconvore/test/data/account_groups_create.json' |
406 | --- txconvore/test/data/account_groups_create.json 1970-01-01 00:00:00 +0000 |
407 | +++ txconvore/test/data/account_groups_create.json 2011-04-13 16:28:00 +0000 |
408 | @@ -0,0 +1,18 @@ |
409 | +{ |
410 | + "group": { |
411 | + "creator": { |
412 | + "id": "XXX", |
413 | + "img": "XXX", |
414 | + "url": "XXX", |
415 | + "username": "XXX" |
416 | + }, |
417 | + "date_created": 1302707186.200645, |
418 | + "date_latest_message": 1302707186.2006581, |
419 | + "id": "XXX", |
420 | + "kind": "private", |
421 | + "members_count": 1, |
422 | + "name": "xxx", |
423 | + "slug": "xxx", |
424 | + "url": "XXX" |
425 | + } |
426 | +} |
427 | |
428 | === added file 'txconvore/test/data/account_mentions.json' |
429 | --- txconvore/test/data/account_mentions.json 1970-01-01 00:00:00 +0000 |
430 | +++ txconvore/test/data/account_mentions.json 2011-04-13 16:28:00 +0000 |
431 | @@ -0,0 +1,51 @@ |
432 | +{ |
433 | + "mentions": [ |
434 | + { |
435 | + "message": { |
436 | + "date_created": 1301096699.259259, |
437 | + "embeds": [], |
438 | + "id": "XXX", |
439 | + "message": "@jonathanj Does it do static inspection though", |
440 | + "stars": [], |
441 | + "user": { |
442 | + "id": "XXX", |
443 | + "img": "XXX", |
444 | + "url": "XXX", |
445 | + "username": "lvh" |
446 | + } |
447 | + }, |
448 | + "topic": { |
449 | + "date_created": 1301007329.5264051, |
450 | + "date_latest_message": 1301109138.3067639, |
451 | + "id": "XXX", |
452 | + "name": "Programmatically building __all__ with a decorator", |
453 | + "slug": "programmatically-building-__all__-with-a-decorator", |
454 | + "url": "XXX" |
455 | + } |
456 | + }, |
457 | + { |
458 | + "message": { |
459 | + "date_created": 1301072572.6535151, |
460 | + "embeds": [], |
461 | + "id": "XXX", |
462 | + "message": "(also @jonathanj is awesome)", |
463 | + "stars": [], |
464 | + "user": { |
465 | + "id": "XXX", |
466 | + "img": "XXX", |
467 | + "url": "XXX", |
468 | + "username": "mithrandi" |
469 | + } |
470 | + }, |
471 | + "topic": { |
472 | + "date_created": 1301072549.0416789, |
473 | + "date_latest_message": 1301099446.2922111, |
474 | + "id": "XXX", |
475 | + "name": "txConvore", |
476 | + "slug": "txconvore", |
477 | + "url": "XXX" |
478 | + } |
479 | + } |
480 | + ], |
481 | + "unread": 0 |
482 | +} |
483 | |
484 | === added file 'txconvore/test/data/account_online.json' |
485 | --- txconvore/test/data/account_online.json 1970-01-01 00:00:00 +0000 |
486 | +++ txconvore/test/data/account_online.json 2011-04-13 16:28:00 +0000 |
487 | @@ -0,0 +1,15 @@ |
488 | +{ |
489 | + "count": 1, |
490 | + "online": [ |
491 | + { |
492 | + "bio": "Foo", |
493 | + "id": "XXX", |
494 | + "img": "XXX", |
495 | + "location": "Earth", |
496 | + "name": "Chuck Whatshisface", |
497 | + "url": "XXX", |
498 | + "username": "XXX", |
499 | + "web": "" |
500 | + } |
501 | + ] |
502 | +} |
503 | |
504 | === added file 'txconvore/test/data/account_verify.json' |
505 | --- txconvore/test/data/account_verify.json 1970-01-01 00:00:00 +0000 |
506 | +++ txconvore/test/data/account_verify.json 2011-04-13 16:28:00 +0000 |
507 | @@ -0,0 +1,6 @@ |
508 | +{ |
509 | + "id": "XXX", |
510 | + "img": "XXX", |
511 | + "url": "XXX", |
512 | + "username": "jonathanj" |
513 | +} |
514 | |
515 | === added file 'txconvore/test/data/groups.json' |
516 | --- txconvore/test/data/groups.json 1970-01-01 00:00:00 +0000 |
517 | +++ txconvore/test/data/groups.json 2011-04-13 16:28:00 +0000 |
518 | @@ -0,0 +1,18 @@ |
519 | +{ |
520 | + "group": { |
521 | + "creator": { |
522 | + "id": "XXX", |
523 | + "img": "XXX", |
524 | + "url": "XXX", |
525 | + "username": "x" |
526 | + }, |
527 | + "date_created": 1300949568.68558, |
528 | + "date_latest_message": 1301574176.861922, |
529 | + "id": "4321", |
530 | + "kind": "public", |
531 | + "members_count": 42, |
532 | + "name": "agroup", |
533 | + "slug": "agroup", |
534 | + "url": "XXX" |
535 | + } |
536 | +} |
537 | |
538 | === added file 'txconvore/test/data/groups_members.json' |
539 | --- txconvore/test/data/groups_members.json 1970-01-01 00:00:00 +0000 |
540 | +++ txconvore/test/data/groups_members.json 2011-04-13 16:28:00 +0000 |
541 | @@ -0,0 +1,22 @@ |
542 | +{ |
543 | + "members": [ |
544 | + { |
545 | + "admin": false, |
546 | + "user": { |
547 | + "id": "1", |
548 | + "img": "XXX", |
549 | + "url": "XXX", |
550 | + "username": "aye" |
551 | + } |
552 | + }, |
553 | + { |
554 | + "admin": true, |
555 | + "user": { |
556 | + "id": "2", |
557 | + "img": "XXX", |
558 | + "url": "XXX", |
559 | + "username": "bee" |
560 | + } |
561 | + } |
562 | + ] |
563 | +} |
564 | |
565 | === added file 'txconvore/test/data/groups_online.json' |
566 | --- txconvore/test/data/groups_online.json 1970-01-01 00:00:00 +0000 |
567 | +++ txconvore/test/data/groups_online.json 2011-04-13 16:28:00 +0000 |
568 | @@ -0,0 +1,25 @@ |
569 | +{ |
570 | + "count": 2, |
571 | + "online": [ |
572 | + { |
573 | + "bio": "", |
574 | + "id": "1", |
575 | + "img": "", |
576 | + "location": "", |
577 | + "name": "Aye", |
578 | + "url": "XXX", |
579 | + "username": "aye", |
580 | + "web": "" |
581 | + }, |
582 | + { |
583 | + "bio": "", |
584 | + "id": "2", |
585 | + "img": "", |
586 | + "location": "", |
587 | + "name": "Bee", |
588 | + "url": "XXX", |
589 | + "username": "bee", |
590 | + "web": "" |
591 | + } |
592 | + ] |
593 | +} |
594 | |
595 | === added file 'txconvore/test/data/groups_topics.json' |
596 | --- txconvore/test/data/groups_topics.json 1970-01-01 00:00:00 +0000 |
597 | +++ txconvore/test/data/groups_topics.json 2011-04-13 16:28:00 +0000 |
598 | @@ -0,0 +1,37 @@ |
599 | +{ |
600 | + "topics": [ |
601 | + { |
602 | + "creator": { |
603 | + "id": "XXX", |
604 | + "img": "XXX", |
605 | + "url": "XXX", |
606 | + "username": "XXX" |
607 | + }, |
608 | + "date_created": 1300959156.58742, |
609 | + "date_latest_message": 1301585498.7861631, |
610 | + "id": "1", |
611 | + "message_count": 935, |
612 | + "name": "General Chat", |
613 | + "slug": "general-chat", |
614 | + "unread": 0, |
615 | + "url": "XXX" |
616 | + }, |
617 | + { |
618 | + "creator": { |
619 | + "id": "XXX", |
620 | + "img": "XXX", |
621 | + "url": "XXX", |
622 | + "username": "XXX" |
623 | + }, |
624 | + "date_created": 1301574442.4583809, |
625 | + "date_latest_message": 1301574442.4583919, |
626 | + "id": "2", |
627 | + "message_count": 1, |
628 | + "name": "txConvore Testing", |
629 | + "slug": "txconvore-testing", |
630 | + "unread": 1, |
631 | + "url": "XXX" |
632 | + } |
633 | + ], |
634 | + "until_id": null |
635 | +} |
636 | |
637 | === added file 'txconvore/test/data/groups_topics_create.json' |
638 | --- txconvore/test/data/groups_topics_create.json 1970-01-01 00:00:00 +0000 |
639 | +++ txconvore/test/data/groups_topics_create.json 2011-04-13 16:28:00 +0000 |
640 | @@ -0,0 +1,16 @@ |
641 | +{ |
642 | + "topic": { |
643 | + "creator": { |
644 | + "id": "XXX", |
645 | + "img": "XXX", |
646 | + "url": "XXX", |
647 | + "username": "XXX" |
648 | + }, |
649 | + "date_created": 1301574442.4583809, |
650 | + "date_latest_message": 1301574442.4583919, |
651 | + "id": "1234", |
652 | + "name": "txConvore Testing", |
653 | + "slug": "txconvore-testing", |
654 | + "url": "XXX" |
655 | + } |
656 | +} |
657 | |
658 | === added file 'txconvore/test/data/messages_star.json' |
659 | --- txconvore/test/data/messages_star.json 1970-01-01 00:00:00 +0000 |
660 | +++ txconvore/test/data/messages_star.json 2011-04-13 16:28:00 +0000 |
661 | @@ -0,0 +1,11 @@ |
662 | +{ |
663 | + "star": { |
664 | + "date_created": 1302597756.098841, |
665 | + "user": { |
666 | + "id": "1", |
667 | + "img": "XXX", |
668 | + "url": "XXX", |
669 | + "username": "XXX" |
670 | + } |
671 | + } |
672 | +} |
673 | |
674 | === added file 'txconvore/test/data/messages_unstar.json' |
675 | --- txconvore/test/data/messages_unstar.json 1970-01-01 00:00:00 +0000 |
676 | +++ txconvore/test/data/messages_unstar.json 2011-04-13 16:28:00 +0000 |
677 | @@ -0,0 +1,11 @@ |
678 | +{ |
679 | + "unstar": { |
680 | + "date_created": 1302597756.098841, |
681 | + "user": { |
682 | + "id": "1", |
683 | + "img": "XXX", |
684 | + "url": "XXX", |
685 | + "username": "XXX" |
686 | + } |
687 | + } |
688 | +} |
689 | |
690 | === added file 'txconvore/test/data/topics_edit.json' |
691 | --- txconvore/test/data/topics_edit.json 1970-01-01 00:00:00 +0000 |
692 | +++ txconvore/test/data/topics_edit.json 2011-04-13 16:28:00 +0000 |
693 | @@ -0,0 +1,16 @@ |
694 | +{ |
695 | + "topic": { |
696 | + "creator": { |
697 | + "id": "XXX", |
698 | + "img": "XXX", |
699 | + "url": "XXX", |
700 | + "username": "XXX" |
701 | + }, |
702 | + "date_created": 1301574442.4583809, |
703 | + "date_latest_message": 1301574442.4583919, |
704 | + "id": "XXX", |
705 | + "name": "New Name", |
706 | + "slug": "test", |
707 | + "url": "XXX" |
708 | + } |
709 | +} |
710 | |
711 | === added file 'txconvore/test/data/topics_info.json' |
712 | --- txconvore/test/data/topics_info.json 1970-01-01 00:00:00 +0000 |
713 | +++ txconvore/test/data/topics_info.json 2011-04-13 16:28:00 +0000 |
714 | @@ -0,0 +1,16 @@ |
715 | +{ |
716 | + "topic": { |
717 | + "creator": { |
718 | + "id": "XXX", |
719 | + "img": "XXX", |
720 | + "url": "XXX", |
721 | + "username": "XXX" |
722 | + }, |
723 | + "date_created": 1300959156.58742, |
724 | + "date_latest_message": 1302596569.9464591, |
725 | + "id": "1", |
726 | + "name": "General Chat", |
727 | + "slug": "general-chat", |
728 | + "url": "XXX" |
729 | + } |
730 | +} |
731 | |
732 | === added file 'txconvore/test/data/topics_messages.json' |
733 | --- txconvore/test/data/topics_messages.json 1970-01-01 00:00:00 +0000 |
734 | +++ txconvore/test/data/topics_messages.json 2011-04-13 16:28:00 +0000 |
735 | @@ -0,0 +1,45 @@ |
736 | + |
737 | +{ |
738 | + "messages": [ |
739 | + { |
740 | + "date_created": 1302596453.564373, |
741 | + "embeds": [], |
742 | + "id": "XXX", |
743 | + "message": "Test message", |
744 | + "stars": [], |
745 | + "user": { |
746 | + "id": "XXX", |
747 | + "img": "XXX", |
748 | + "url": "XXX", |
749 | + "username": "XXX" |
750 | + } |
751 | + }, |
752 | + { |
753 | + "date_created": 1302596569.9341509, |
754 | + "embeds": [], |
755 | + "id": "XXX", |
756 | + "message": "Test response", |
757 | + "stars": [], |
758 | + "user": { |
759 | + "id": "XXX", |
760 | + "img": "XXX", |
761 | + "url": "XXX", |
762 | + "username": "XXX" |
763 | + } |
764 | + }, |
765 | + { |
766 | + "date_created": 1302596724.3432169, |
767 | + "embeds": [], |
768 | + "id": "XXX", |
769 | + "message": "oops, got the wrong ID", |
770 | + "stars": [], |
771 | + "user": { |
772 | + "id": "XXX", |
773 | + "img": "XXX", |
774 | + "url": "XXX", |
775 | + "username": "XXX" |
776 | + } |
777 | + } |
778 | + ], |
779 | + "until_id": "639139" |
780 | +} |
781 | |
782 | === added file 'txconvore/test/data/topics_messages_create.json' |
783 | --- txconvore/test/data/topics_messages_create.json 1970-01-01 00:00:00 +0000 |
784 | +++ txconvore/test/data/topics_messages_create.json 2011-04-13 16:28:00 +0000 |
785 | @@ -0,0 +1,15 @@ |
786 | +{ |
787 | + "message": { |
788 | + "date_created": 1302596453.564373, |
789 | + "embeds": [], |
790 | + "id": "XXX", |
791 | + "message": "Test message", |
792 | + "stars": [], |
793 | + "user": { |
794 | + "id": "XXX", |
795 | + "img": "XXX", |
796 | + "url": "XXX", |
797 | + "username": "XXX" |
798 | + } |
799 | + } |
800 | +} |
801 | |
802 | === added file 'txconvore/test/data/users_info.json' |
803 | --- txconvore/test/data/users_info.json 1970-01-01 00:00:00 +0000 |
804 | +++ txconvore/test/data/users_info.json 2011-04-13 16:28:00 +0000 |
805 | @@ -0,0 +1,12 @@ |
806 | +{ |
807 | + "user": { |
808 | + "bio": "", |
809 | + "id": "1", |
810 | + "img": "XXX", |
811 | + "location": "", |
812 | + "name": "Chuck Finley", |
813 | + "url": "XXX", |
814 | + "username": "Y", |
815 | + "web": "http://web.com/" |
816 | + } |
817 | +} |
818 | |
819 | === added file 'txconvore/test/test_client.py' |
820 | --- txconvore/test/test_client.py 1970-01-01 00:00:00 +0000 |
821 | +++ txconvore/test/test_client.py 2011-04-13 16:28:00 +0000 |
822 | @@ -0,0 +1,697 @@ |
823 | +from twisted.internet.defer import succeed |
824 | +from twisted.python.filepath import FilePath |
825 | +from twisted.trial.unittest import TestCase |
826 | + |
827 | +from txconvore.client import ( |
828 | + Endpoint, BasicCreds, Account, _ConvoreAPI, Group, Topic, Message, User) |
829 | + |
830 | + |
831 | + |
832 | +class MockEndpoint(Endpoint): |
833 | + """ |
834 | + Endpoint with submit mocked out. |
835 | + """ |
836 | + def getPage(self, url, method, postdata=None, headers=None, *a, **kw): |
837 | + """ |
838 | + Store our parameters instead of submitting a request. |
839 | + """ |
840 | + self.url = url |
841 | + self.method = method |
842 | + self.data = postdata |
843 | + self.headers = headers |
844 | + return succeed((self.response, {})) |
845 | + |
846 | + |
847 | + |
848 | +class EndpointTests(TestCase): |
849 | + """ |
850 | + Tests for L{txconvore.client.Endpoint}. |
851 | + """ |
852 | + def test_defaultEndpoint(self): |
853 | + """ |
854 | + L{Endpoint} defaults to the real global Convore endpoint. |
855 | + """ |
856 | + endpoint = Endpoint() |
857 | + self.assertEqual(endpoint.baseURL, 'https://convore.com/api/') |
858 | + |
859 | + |
860 | + def test_getDefaultRoot(self): |
861 | + """ |
862 | + The default root URL has no API version. |
863 | + """ |
864 | + endpoint = Endpoint() |
865 | + self.assertEqual(endpoint.getRootURL(), endpoint.baseURL) |
866 | + |
867 | + |
868 | + def test_versionedRoot(self): |
869 | + """ |
870 | + The root URL must include the API version if specified. |
871 | + """ |
872 | + endpoint = Endpoint('http://convore.test.url/', '20090817') |
873 | + self.assertEqual(endpoint.getRootURL(), |
874 | + 'http://convore.test.url/20090817/') |
875 | + |
876 | + |
877 | + def test_basicAuth(self): |
878 | + """ |
879 | + An endpoint constructed with basic auth credentials sends them along |
880 | + with every request. |
881 | + """ |
882 | + creds = BasicCreds('testuser', 'password') |
883 | + endpoint = MockEndpoint('http://convore.test.url/', creds=creds) |
884 | + endpoint.response = '' |
885 | + endpoint.submit('http://convore.test.url/blah', 'GET') |
886 | + self.assertEqual(endpoint.headers['Authorization'], |
887 | + 'Basic dGVzdHVzZXI6cGFzc3dvcmQ=') |
888 | + |
889 | + |
890 | + def test_unsuppportedAuth(self): |
891 | + """ |
892 | + Constructing an endpoint with an unsupported credentials object raises |
893 | + an exception. |
894 | + """ |
895 | + self.assertRaises(ValueError, Endpoint, creds=42) |
896 | + |
897 | + |
898 | + |
899 | +class ConvoreAPITests(TestCase): |
900 | + """ |
901 | + Tests for L{txconvore.client._ConvoreAPI}. |
902 | + """ |
903 | + def setUp(self): |
904 | + creds = BasicCreds('testuser', 'password') |
905 | + self.endpoint = MockEndpoint('http://convore.test.url/api/', |
906 | + creds=creds) |
907 | + |
908 | + |
909 | + def test_getURL(self): |
910 | + """ |
911 | + Constructing an endpoint URL uses the I{collectionName} attribute as a |
912 | + prefix unless the I{prefix} parameter is given. If the I{suffix} |
913 | + parameter is given those paths are appended to the URL. The I{params} |
914 | + parameter specifies URL query parameters. |
915 | + """ |
916 | + c = _ConvoreAPI() |
917 | + c.collectionName = 'a' |
918 | + self.assertEqual( |
919 | + c.getURL(self.endpoint), |
920 | + 'http://convore.test.url/api/a') |
921 | + self.assertEqual( |
922 | + c.getURL(self.endpoint, |
923 | + prefix=['b'], |
924 | + suffix=['c'], |
925 | + params=dict(quux='baz')), |
926 | + 'http://convore.test.url/api/b/c?quux=baz') |
927 | + |
928 | + |
929 | + def test_getURLWithPrefix(self): |
930 | + """ |
931 | + Construct an endpoint URL overriding the URL prefix. |
932 | + """ |
933 | + c = _ConvoreAPI() |
934 | + c.collectionName = 'a' |
935 | + self.assertEqual( |
936 | + c.getURL(self.endpoint, prefix=['b']), |
937 | + 'http://convore.test.url/api/b') |
938 | + self.assertEqual( |
939 | + c.getURL(self.endpoint, prefix=['c', 'd']), |
940 | + 'http://convore.test.url/api/c/d') |
941 | + |
942 | + |
943 | + def test_getURLWithSuffix(self): |
944 | + """ |
945 | + Construct an endpoint URL specifying the URL suffix. |
946 | + """ |
947 | + c = _ConvoreAPI() |
948 | + c.collectionName = 'a' |
949 | + self.assertEqual( |
950 | + c.getURL(self.endpoint, suffix=['b']), |
951 | + 'http://convore.test.url/api/a/b') |
952 | + self.assertEqual( |
953 | + c.getURL(self.endpoint, suffix=['b', 'c']), |
954 | + 'http://convore.test.url/api/a/b/c') |
955 | + |
956 | + |
957 | + def test_getURLWithPrefixAndSuffix(self): |
958 | + """ |
959 | + Construct an endpoint URL overriding the URL prefix and specifying the |
960 | + URL suffix. |
961 | + """ |
962 | + c = _ConvoreAPI() |
963 | + c.collectionName = 'a' |
964 | + self.assertEqual( |
965 | + c.getURL(self.endpoint, prefix=['b', 'c'], suffix=['d']), |
966 | + 'http://convore.test.url/api/b/c/d') |
967 | + self.assertEqual( |
968 | + c.getURL(self.endpoint, prefix=['b'], suffix=['c', 'd']), |
969 | + 'http://convore.test.url/api/b/c/d') |
970 | + self.assertEqual( |
971 | + c.getURL(self.endpoint, |
972 | + prefix=[u'\N{DAGGER}'], |
973 | + suffix=[u'\N{BULLET}']), |
974 | + 'http://convore.test.url/api/%E2%80%A0/%E2%80%A2') |
975 | + |
976 | + |
977 | + def test_getURLWithoutCollectionName(self): |
978 | + """ |
979 | + Constructing a URL without specifying I{collectionName} or I{prefix} |
980 | + raises C{NotImplementedError}. |
981 | + """ |
982 | + c = _ConvoreAPI() |
983 | + self.assertRaises( |
984 | + NotImplementedError, c.getURL, self.endpoint) |
985 | + self.assertEqual( |
986 | + c.getURL(self.endpoint, prefix=['a']), |
987 | + 'http://convore.test.url/api/a') |
988 | + |
989 | + |
990 | + def test_getURLParams(self): |
991 | + """ |
992 | + Construct an endpoint URL specifying URL query parameters. |
993 | + """ |
994 | + c = _ConvoreAPI() |
995 | + c.collectionName = 'a' |
996 | + self.assertEqual( |
997 | + c.getURL( |
998 | + self.endpoint, |
999 | + params=dict( |
1000 | + foo='bar')), |
1001 | + 'http://convore.test.url/api/a?foo=bar') |
1002 | + self.assertEqual( |
1003 | + c.getURL( |
1004 | + self.endpoint, |
1005 | + params={ |
1006 | + u'k': u'\N{DAGGER}', |
1007 | + u'\N{BULLET}': u'v'}), |
1008 | + 'http://convore.test.url/api/a?k=%25E2%2580%25A0&%25E2%2580%25A2=v') |
1009 | + |
1010 | + |
1011 | + |
1012 | +class ConvoreAPITestCase(TestCase): |
1013 | + def setUp(self): |
1014 | + creds = BasicCreds('testuser', 'password') |
1015 | + self.endpoint = MockEndpoint( |
1016 | + 'http://convore.test.url/api/', creds=creds) |
1017 | + |
1018 | + |
1019 | + def getTestData(self, filename): |
1020 | + """ |
1021 | + Open a test data file, in the C{test/data} directory, by name and |
1022 | + return its contents. |
1023 | + """ |
1024 | + dataPath = FilePath(__file__).sibling('data').child(filename) |
1025 | + return dataPath.open('rb').read() |
1026 | + |
1027 | + |
1028 | + def callAPI(self, method, url, func, *a, **kw): |
1029 | + """ |
1030 | + Call C{func}, a txConvore API function, verifying the HTTP method on |
1031 | + the endpoint and the URL the endpoint contacted match C{method} and |
1032 | + C{url} respectively. |
1033 | + """ |
1034 | + d = func(self.endpoint, *a, **kw) |
1035 | + self.assertEqual(self.endpoint.method, method) |
1036 | + self.assertEqual(self.endpoint.url, url) |
1037 | + return d |
1038 | + |
1039 | + |
1040 | + |
1041 | +class AccountTests(ConvoreAPITestCase): |
1042 | + """ |
1043 | + Tests for L{txconvore.client.Account}. |
1044 | + """ |
1045 | + def setUp(self): |
1046 | + super(AccountTests, self).setUp() |
1047 | + self.account = Account() |
1048 | + |
1049 | + |
1050 | + def test_createGroup(self): |
1051 | + """ |
1052 | + Create a new group. |
1053 | + """ |
1054 | + self.endpoint.response = self.getTestData('account_groups_create.json') |
1055 | + d = self.callAPI( |
1056 | + 'POST', |
1057 | + 'http://convore.test.url/api/groups/create.json', |
1058 | + self.account.createGroup, u'xxx', u'private') |
1059 | + |
1060 | + @d.addCallback |
1061 | + def gotData(data): |
1062 | + self.assertEqual(data[u'group'][u'name'], u'xxx') |
1063 | + self.assertEqual(data[u'group'][u'kind'], u'private') |
1064 | + |
1065 | + return d |
1066 | + |
1067 | + |
1068 | + def test_groups(self): |
1069 | + """ |
1070 | + Get a list of the current user's groups. |
1071 | + """ |
1072 | + self.endpoint.response = self.getTestData('account_groups.json') |
1073 | + d = self.callAPI( |
1074 | + 'GET', |
1075 | + 'http://convore.test.url/api/groups.json', |
1076 | + self.account.groups) |
1077 | + |
1078 | + @d.addCallback |
1079 | + def gotData(data): |
1080 | + self.assertEqual(len(data['groups']), 3) |
1081 | + |
1082 | + return d |
1083 | + |
1084 | + |
1085 | + def test_markRead(self): |
1086 | + """ |
1087 | + Mark all messages as read. |
1088 | + """ |
1089 | + self.endpoint.response = '' |
1090 | + return self.callAPI( |
1091 | + 'GET', |
1092 | + 'http://convore.test.url/api/account/mark_read.json', |
1093 | + self.account.markRead) |
1094 | + |
1095 | + |
1096 | + def test_mentions(self): |
1097 | + """ |
1098 | + Get the user's mentions. |
1099 | + """ |
1100 | + self.endpoint.response = self.getTestData('account_mentions.json') |
1101 | + d = self.callAPI( |
1102 | + 'GET', |
1103 | + 'http://convore.test.url/api/account/mentions.json', |
1104 | + self.account.mentions) |
1105 | + |
1106 | + @d.addCallback |
1107 | + def gotData(data): |
1108 | + self.assertEqual(len(data['mentions']), 2) |
1109 | + |
1110 | + return d |
1111 | + |
1112 | + |
1113 | + def test_online(self): |
1114 | + """ |
1115 | + Get members online now. |
1116 | + """ |
1117 | + self.endpoint.response = self.getTestData('account_online.json') |
1118 | + d = self.callAPI( |
1119 | + 'GET', |
1120 | + 'http://convore.test.url/api/account/online.json', |
1121 | + self.account.online) |
1122 | + |
1123 | + @d.addCallback |
1124 | + def gotData(data): |
1125 | + self.assertEqual(len(data['online']), 1) |
1126 | + self.assertEqual(data['count'], 1) |
1127 | + |
1128 | + return d |
1129 | + |
1130 | + |
1131 | + def test_verify(self): |
1132 | + """ |
1133 | + Determine whether the user is properly logged in. |
1134 | + """ |
1135 | + self.endpoint.response = self.getTestData('account_verify.json') |
1136 | + d = self.callAPI( |
1137 | + 'GET', |
1138 | + 'http://convore.test.url/api/account/verify.json', |
1139 | + self.account.verify) |
1140 | + |
1141 | + @d.addCallback |
1142 | + def gotData(data): |
1143 | + self.assertEqual(data['username'], u'jonathanj') |
1144 | + |
1145 | + return d |
1146 | + |
1147 | + |
1148 | + |
1149 | +class UserTests(ConvoreAPITestCase): |
1150 | + """ |
1151 | + Tests for L{txconvore.client.User}. |
1152 | + """ |
1153 | + def setUp(self): |
1154 | + super(UserTests, self).setUp() |
1155 | + self.user = User(u'1') |
1156 | + |
1157 | + |
1158 | + def test_info(self): |
1159 | + """ |
1160 | + Get detailed information about the user. |
1161 | + """ |
1162 | + self.endpoint.response = self.getTestData('users_info.json') |
1163 | + d = self.callAPI( |
1164 | + 'GET', |
1165 | + 'http://convore.test.url/api/users/1.json', |
1166 | + self.user.info) |
1167 | + |
1168 | + @d.addCallback |
1169 | + def gotData(data): |
1170 | + self.assertEqual(data['user']['id'], u'1') |
1171 | + self.assertEqual(data['user']['name'], u'Chuck Finley') |
1172 | + self.assertEqual(data['user']['username'], u'Y') |
1173 | + |
1174 | + return d |
1175 | + |
1176 | + |
1177 | + |
1178 | +class GroupTests(ConvoreAPITestCase): |
1179 | + def setUp(self): |
1180 | + super(GroupTests, self).setUp() |
1181 | + self.group = Group(u'4321') |
1182 | + |
1183 | + |
1184 | + def test_createTopic(self): |
1185 | + """ |
1186 | + Create a new topic. |
1187 | + """ |
1188 | + self.endpoint.response = self.getTestData('groups_topics_create.json') |
1189 | + d = self.callAPI( |
1190 | + 'POST', |
1191 | + 'http://convore.test.url/api/groups/4321/topics/create.json', |
1192 | + self.group.createTopic, u'txConvore Testing') |
1193 | + |
1194 | + @d.addCallback |
1195 | + def gotData(data): |
1196 | + self.assertEqual(data['topic']['id'], u'1234') |
1197 | + self.assertEqual(data['topic']['name'], u'txConvore Testing') |
1198 | + |
1199 | + return d |
1200 | + |
1201 | + |
1202 | + def test_info(self): |
1203 | + """ |
1204 | + Get detailed information about the group. |
1205 | + """ |
1206 | + self.endpoint.response = self.getTestData('groups.json') |
1207 | + d = self.callAPI( |
1208 | + 'GET', |
1209 | + 'http://convore.test.url/api/groups/4321.json', |
1210 | + self.group.info) |
1211 | + |
1212 | + @d.addCallback |
1213 | + def gotData(data): |
1214 | + self.assertEqual(data['group']['creator']['username'], u'x') |
1215 | + self.assertEqual(data['group']['id'], u'4321') |
1216 | + self.assertEqual(data['group']['name'], u'agroup') |
1217 | + self.assertEqual(data['group']['slug'], u'agroup') |
1218 | + |
1219 | + return d |
1220 | + |
1221 | + |
1222 | + def test_join(self): |
1223 | + """ |
1224 | + Join a public group. |
1225 | + """ |
1226 | + self.endpoint.response = '{"message": "Group joined."}' |
1227 | + d = self.callAPI( |
1228 | + 'POST', |
1229 | + 'http://convore.test.url/api/groups/4321/join.json', |
1230 | + self.group.join) |
1231 | + return d |
1232 | + |
1233 | + |
1234 | + def test_leave(self): |
1235 | + """ |
1236 | + Leave a group. |
1237 | + """ |
1238 | + self.endpoint.response = '{"message": "Group left."}' |
1239 | + d = self.callAPI( |
1240 | + 'POST', |
1241 | + 'http://convore.test.url/api/groups/4321/leave.json', |
1242 | + self.group.leave) |
1243 | + return d |
1244 | + |
1245 | + |
1246 | + def test_markRead(self): |
1247 | + """ |
1248 | + Mark all messages in the group as read. |
1249 | + """ |
1250 | + self.endpoint.response = '{"message": "Group marked as read."}' |
1251 | + d = self.callAPI( |
1252 | + 'POST', |
1253 | + 'http://convore.test.url/api/groups/4321/mark_read.json', |
1254 | + self.group.markRead) |
1255 | + return d |
1256 | + |
1257 | + |
1258 | + def test_members(self): |
1259 | + """ |
1260 | + Get all the members of a group. |
1261 | + """ |
1262 | + self.endpoint.response = self.getTestData('groups_members.json') |
1263 | + d = self.callAPI( |
1264 | + 'GET', |
1265 | + 'http://convore.test.url/api/groups/4321/members.json', |
1266 | + self.group.members) |
1267 | + |
1268 | + @d.addCallback |
1269 | + def gotData(data): |
1270 | + members = data['members'] |
1271 | + self.assertEqual(len(members), 2) |
1272 | + self.assertEqual(members[0]['admin'], False) |
1273 | + self.assertEqual(members[0]['user']['id'], u'1') |
1274 | + self.assertEqual(members[1]['admin'], True) |
1275 | + self.assertEqual(members[1]['user']['id'], u'2') |
1276 | + |
1277 | + return d |
1278 | + |
1279 | + |
1280 | + def test_membersFilter(self): |
1281 | + """ |
1282 | + Get all the members of a group, requesting only certain member types. |
1283 | + """ |
1284 | + self.endpoint.response = '' |
1285 | + d = self.callAPI( |
1286 | + 'GET', |
1287 | + 'http://convore.test.url/api/groups/4321/members.json?filter=admin', |
1288 | + self.group.members, filter=u'admin') |
1289 | + return d |
1290 | + |
1291 | + |
1292 | + def test_onlineMembers(self): |
1293 | + """ |
1294 | + Get group members online now. |
1295 | + """ |
1296 | + self.endpoint.response = self.getTestData('groups_online.json') |
1297 | + d = self.callAPI( |
1298 | + 'GET', |
1299 | + 'http://convore.test.url/api/groups/4321/online.json', |
1300 | + self.group.onlineMembers) |
1301 | + |
1302 | + @d.addCallback |
1303 | + def gotData(data): |
1304 | + online = data['online'] |
1305 | + self.assertEqual(len(online), 2) |
1306 | + self.assertEqual(data['count'], len(online)) |
1307 | + self.assertEqual(online[0]['id'], u'1') |
1308 | + self.assertEqual(online[1]['id'], u'2') |
1309 | + |
1310 | + return d |
1311 | + |
1312 | + |
1313 | + def test_request(self): |
1314 | + """ |
1315 | + Request to join a private group. |
1316 | + """ |
1317 | + self.endpoint.response = '{"message": "Something something."}' |
1318 | + d = self.callAPI( |
1319 | + 'POST', |
1320 | + 'http://convore.test.url/api/groups/4321/request.json', |
1321 | + self.group.request) |
1322 | + return d |
1323 | + |
1324 | + |
1325 | + def test_topics(self): |
1326 | + """ |
1327 | + Get the latest topics in a group. |
1328 | + """ |
1329 | + self.endpoint.response = self.getTestData('groups_topics.json') |
1330 | + d = self.callAPI( |
1331 | + 'GET', |
1332 | + 'http://convore.test.url/api/groups/4321/topics.json', |
1333 | + self.group.topics) |
1334 | + |
1335 | + @d.addCallback |
1336 | + def gotData(data): |
1337 | + topics = data['topics'] |
1338 | + self.assertEqual(len(topics), 2) |
1339 | + self.assertEqual(topics[0]['id'], u'1') |
1340 | + self.assertEqual(topics[1]['id'], u'2') |
1341 | + |
1342 | + return d |
1343 | + |
1344 | + |
1345 | + |
1346 | +class TopicTests(ConvoreAPITestCase): |
1347 | + def setUp(self): |
1348 | + super(TopicTests, self).setUp() |
1349 | + self.topic = Topic(u'1') |
1350 | + |
1351 | + |
1352 | + def test_createMessage(self): |
1353 | + """ |
1354 | + Post a new message. |
1355 | + """ |
1356 | + self.endpoint.response = self.getTestData('topics_messages_create.json') |
1357 | + d = self.callAPI( |
1358 | + 'POST', |
1359 | + 'http://convore.test.url/api/topics/1/messages/create.json', |
1360 | + self.topic.createMessage, u'Test message') |
1361 | + |
1362 | + @d.addCallback |
1363 | + def gotData(data): |
1364 | + message = data[u'message'] |
1365 | + self.assertEqual(message[u'message'], u'Test message') |
1366 | + |
1367 | + return d |
1368 | + |
1369 | + |
1370 | + def test_delete(self): |
1371 | + """ |
1372 | + Delete a topic. |
1373 | + """ |
1374 | + self.endpoint.response = '{"message": "Something something."}' |
1375 | + d = self.callAPI( |
1376 | + 'POST', |
1377 | + 'http://convore.test.url/api/topics/1/delete.json', |
1378 | + self.topic.delete) |
1379 | + return d |
1380 | + |
1381 | + |
1382 | + def test_edit(self): |
1383 | + """ |
1384 | + Delete a topic. |
1385 | + """ |
1386 | + self.endpoint.response = self.getTestData('topics_edit.json') |
1387 | + d = self.callAPI( |
1388 | + 'POST', |
1389 | + 'http://convore.test.url/api/topics/1/edit.json', |
1390 | + self.topic.edit, name=u'New Name') |
1391 | + return d |
1392 | + |
1393 | + |
1394 | + def test_info(self): |
1395 | + """ |
1396 | + Get detailed information about the topic. |
1397 | + """ |
1398 | + self.endpoint.response = self.getTestData('topics_info.json') |
1399 | + d = self.callAPI( |
1400 | + 'GET', |
1401 | + 'http://convore.test.url/api/topics/1.json', |
1402 | + self.topic.info) |
1403 | + |
1404 | + @d.addCallback |
1405 | + def gotData(data): |
1406 | + self.assertEqual(data[u'topic'][u'id'], u'1') |
1407 | + |
1408 | + return d |
1409 | + |
1410 | + |
1411 | + def test_markRead(self): |
1412 | + """ |
1413 | + Mark all messages in a topic as read. |
1414 | + """ |
1415 | + self.endpoint.response = '{"message": "Something something."}' |
1416 | + d = self.callAPI( |
1417 | + 'POST', |
1418 | + 'http://convore.test.url/api/topics/1/mark_read.json', |
1419 | + self.topic.markRead) |
1420 | + return d |
1421 | + |
1422 | + |
1423 | + def test_messages(self): |
1424 | + """ |
1425 | + Get the latest messages in a topic. |
1426 | + """ |
1427 | + self.endpoint.response = self.getTestData('topics_messages.json') |
1428 | + d = self.callAPI( |
1429 | + 'GET', |
1430 | + 'http://convore.test.url/api/topics/1/messages.json?mark_read=false', |
1431 | + self.topic.messages) |
1432 | + |
1433 | + @d.addCallback |
1434 | + def gotData(data): |
1435 | + self.assertEqual(len(data[u'messages']), 3) |
1436 | + self.assertEqual(data[u'until_id'], u'639139') |
1437 | + |
1438 | + return d |
1439 | + |
1440 | + |
1441 | + def test_messagesMarkRead(self): |
1442 | + """ |
1443 | + Get the latest messages in a topic. |
1444 | + """ |
1445 | + self.endpoint.response = '{"message": "Something something."}' |
1446 | + d = self.callAPI( |
1447 | + 'GET', |
1448 | + 'http://convore.test.url/api/topics/1/messages.json?mark_read=true', |
1449 | + self.topic.messages, markRead=True) |
1450 | + return d |
1451 | + |
1452 | + |
1453 | + def test_messagesUntilMessageID(self): |
1454 | + """ |
1455 | + Get the latest messages in a topic. |
1456 | + """ |
1457 | + self.endpoint.response = '{"message": "Something something."}' |
1458 | + d = self.callAPI( |
1459 | + 'GET', |
1460 | + 'http://convore.test.url/api/topics/1/messages.json?until_id=42&mark_read=false', |
1461 | + self.topic.messages, untilMessageID=u'42') |
1462 | + return d |
1463 | + |
1464 | + |
1465 | + |
1466 | +class MessageTests(ConvoreAPITestCase): |
1467 | + def setUp(self): |
1468 | + super(MessageTests, self).setUp() |
1469 | + self.message = Message(u'21') |
1470 | + |
1471 | + |
1472 | + def test_star(self): |
1473 | + """ |
1474 | + Star a message. If the message has already been starred by this user, |
1475 | + this endpoint will then unstar the message. |
1476 | + """ |
1477 | + self.endpoint.response = self.getTestData('messages_star.json') |
1478 | + d = self.callAPI( |
1479 | + 'POST', |
1480 | + 'http://convore.test.url/api/messages/21/star.json', |
1481 | + self.message.star) |
1482 | + |
1483 | + @d.addCallback |
1484 | + def gotData(data): |
1485 | + self.assertTrue(data.get(u'star') is not None) |
1486 | + self.assertEqual(data[u'star'][u'user'][u'id'], u'1') |
1487 | + |
1488 | + return d |
1489 | + |
1490 | + |
1491 | + def test_unstar(self): |
1492 | + """ |
1493 | + Star a message. If the message has already been starred by this user, |
1494 | + this endpoint will then unstar the message. |
1495 | + """ |
1496 | + self.endpoint.response = self.getTestData('messages_unstar.json') |
1497 | + d = self.callAPI( |
1498 | + 'POST', |
1499 | + 'http://convore.test.url/api/messages/21/star.json', |
1500 | + self.message.star) |
1501 | + |
1502 | + @d.addCallback |
1503 | + def gotData(data): |
1504 | + self.assertTrue(data.get(u'unstar') is not None) |
1505 | + self.assertEqual(data['unstar'][u'user'][u'id'], u'1') |
1506 | + |
1507 | + return d |
1508 | + |
1509 | + |
1510 | + def test_delete(self): |
1511 | + """ |
1512 | + Delete a topic. |
1513 | + """ |
1514 | + self.endpoint.response = '{"message": "Message deleted."}' |
1515 | + d = self.callAPI( |
1516 | + 'POST', |
1517 | + 'http://convore.test.url/api/messages/21/delete.json', |
1518 | + self.message.delete) |
1519 | + return d |
1520 | |
1521 | === added file 'txconvore/test/test_util.py' |
1522 | --- txconvore/test/test_util.py 1970-01-01 00:00:00 +0000 |
1523 | +++ txconvore/test/test_util.py 2011-04-13 16:28:00 +0000 |
1524 | @@ -0,0 +1,148 @@ |
1525 | +from twisted.internet.defer import fail, succeed |
1526 | +from twisted.trial.unittest import TestCase |
1527 | +from twisted.web import client, http, error as weberror |
1528 | + |
1529 | +from txconvore.util import PerseverantDownloader |
1530 | + |
1531 | + |
1532 | + |
1533 | +class PerseverantDownloaderTests(TestCase): |
1534 | + """ |
1535 | + Tests for L{txconvore.util.PerseverantDownloader}. |
1536 | + """ |
1537 | + def wrapPageGetter(self, func, response_headers=None): |
1538 | + """ |
1539 | + Wrap a function returning a C{Deferred} with something suitable to |
1540 | + replace L{PerseverantDownloader._getPage}. |
1541 | + """ |
1542 | + def _getPage(*a, **kw): |
1543 | + fact = client.HTTPClientFactory('a_url') |
1544 | + fact.response_headers = response_headers |
1545 | + return func(), fact |
1546 | + return _getPage |
1547 | + |
1548 | + |
1549 | + def test_partialContent(self): |
1550 | + """ |
1551 | + Return any partial content when HTTP 206 is returned. |
1552 | + """ |
1553 | + def _partialContent(): |
1554 | + return fail(weberror.Error( |
1555 | + http.PARTIAL_CONTENT, |
1556 | + response='partial content')) |
1557 | + |
1558 | + downloader = PerseverantDownloader('a_url') |
1559 | + downloader._getPage = self.wrapPageGetter(_partialContent) |
1560 | + d = downloader.download() |
1561 | + |
1562 | + @d.addCallback |
1563 | + def gotData((data, headers)): |
1564 | + self.assertEqual(data, 'partial content') |
1565 | + |
1566 | + return d |
1567 | + |
1568 | + |
1569 | + def test_brokenError(self): |
1570 | + """ |
1571 | + If a C{twisted.web.error.Error} with a non-stringified integer as its |
1572 | + C{status} occurs then no retrying or partial content handling occurs, |
1573 | + the Failure is simply returned along the Deferred chain. |
1574 | + """ |
1575 | + def _brokenThing(): |
1576 | + self.called += 1 |
1577 | + return fail(weberror.Error('')) |
1578 | + |
1579 | + self.called = 0 |
1580 | + downloader = PerseverantDownloader('a_url') |
1581 | + downloader._getPage = self.wrapPageGetter(_brokenThing) |
1582 | + d = downloader.download() |
1583 | + |
1584 | + @d.addCallback |
1585 | + def gotData((data, headers)): |
1586 | + self.fail('Expected errback') |
1587 | + |
1588 | + @d.addErrback |
1589 | + def gotFailure(f): |
1590 | + f.trap(weberror.Error) |
1591 | + self.assertEqual(f.value.status, '') |
1592 | + self.assertEqual(self.called, 1) |
1593 | + |
1594 | + return d |
1595 | + |
1596 | + |
1597 | + def test_retryableHTTPError(self): |
1598 | + """ |
1599 | + If a fetch fails with an error code present in |
1600 | + L{PerseverantDownloader.retryableHTTPCodes} then the fetch is retried |
1601 | + L{PerseverantDownloader.tries} times, eventually failing. |
1602 | + """ |
1603 | + def _retryableWebError(): |
1604 | + self.called += 1 |
1605 | + return fail(weberror.Error(errorCode)) |
1606 | + |
1607 | + self.called = 0 |
1608 | + tries = 3 |
1609 | + downloader = PerseverantDownloader('a_url', tries=tries) |
1610 | + errorCode = http.INTERNAL_SERVER_ERROR |
1611 | + self.assertIn(errorCode, downloader.retryableHTTPCodes) |
1612 | + # Let's not make the tests take longer than they have to. |
1613 | + downloader.factor = 0 |
1614 | + downloader._getPage = self.wrapPageGetter(_retryableWebError) |
1615 | + d = self.assertFailure(downloader.download(), weberror.Error) |
1616 | + |
1617 | + @d.addCallback |
1618 | + def checkError(e): |
1619 | + failures = self.flushLoggedErrors(weberror.Error) |
1620 | + self.assertEqual(len(failures), tries) |
1621 | + self.assertEqual(self.called, tries) |
1622 | + self.assertEqual(e.status, errorCode) |
1623 | + |
1624 | + return d |
1625 | + |
1626 | + |
1627 | + def test_notRetryableHTTPError(self): |
1628 | + """ |
1629 | + If a fetch fails with an error code B{not} present in |
1630 | + L{PerseverantDownloader.retryableHTTPCodes} then no retry attempt is |
1631 | + made. |
1632 | + """ |
1633 | + def _notRetryableWeberror(): |
1634 | + self.called += 1 |
1635 | + return fail(weberror.Error(errorCode)) |
1636 | + |
1637 | + self.called = 0 |
1638 | + tries = 3 |
1639 | + downloader = PerseverantDownloader('a_url', tries=tries) |
1640 | + errorCode = http.NOT_FOUND |
1641 | + self.assertNotIn(errorCode, downloader.retryableHTTPCodes) |
1642 | + downloader._getPage = self.wrapPageGetter(_notRetryableWeberror) |
1643 | + d = self.assertFailure(downloader.download(), weberror.Error) |
1644 | + |
1645 | + @d.addCallback |
1646 | + def checkError(e): |
1647 | + self.assertEqual(self.called, 1) |
1648 | + self.assertEqual(e.status, errorCode) |
1649 | + |
1650 | + return d |
1651 | + |
1652 | + |
1653 | + def test_download(self): |
1654 | + """ |
1655 | + L{PerseverantDownloader.download} fetches the data at the given URL and |
1656 | + returns it along with the response headers. |
1657 | + """ |
1658 | + def _getPage(): |
1659 | + return succeed('some data') |
1660 | + |
1661 | + downloader = PerseverantDownloader('a_url') |
1662 | + downloader._getPage = self.wrapPageGetter( |
1663 | + _getPage, |
1664 | + response_headers=dict(foo=['bar'])) |
1665 | + d = downloader.download() |
1666 | + |
1667 | + @d.addCallback |
1668 | + def checkData((data, headers)): |
1669 | + self.assertEqual(data, 'some data') |
1670 | + self.assertEqual(headers.get('foo'), ['bar']) |
1671 | + |
1672 | + return d |
1673 | |
1674 | === added file 'txconvore/util.py' |
1675 | --- txconvore/util.py 1970-01-01 00:00:00 +0000 |
1676 | +++ txconvore/util.py 2011-04-13 16:28:00 +0000 |
1677 | @@ -0,0 +1,190 @@ |
1678 | +import time |
1679 | +import urllib |
1680 | + |
1681 | +from twisted.internet import reactor, task, error as ineterror |
1682 | +from twisted.python import log |
1683 | +from twisted.web import client, http, error as weberror |
1684 | + |
1685 | + |
1686 | + |
1687 | +class PerseverantDownloader(object): |
1688 | + """ |
1689 | + Perseverantly attempt to download a URL. |
1690 | + |
1691 | + Each retry attempt is delayed by L{factor} up to a maximum of L{maxDelay}, |
1692 | + starting at L{initialDelay}. |
1693 | + |
1694 | + @type url: C{str} |
1695 | + @ivar url: HTTP URL to download. |
1696 | + |
1697 | + @type maxDelay: C{float} |
1698 | + @cvar maxDelay: Maximum delay, in seconds, between retry attempts. |
1699 | + |
1700 | + @type initialDelay: C{float} |
1701 | + @cvar initialDelay: Delay before the first retry attempt. |
1702 | + |
1703 | + @type factor: C{float} |
1704 | + @cvar factor: Factor to increase the delay by after each attempt. |
1705 | + |
1706 | + @type retryableHTTPCodes: C{list} |
1707 | + @cvar retryableHTTPCodes: HTTP error codes that suggest the error is |
1708 | + intermittent and that a retry should be attempted. |
1709 | + |
1710 | + @type defaultTimeout: C{float} |
1711 | + @cvar defaultTimeout: Default fetch timeout value. |
1712 | + """ |
1713 | + maxDelay = 3600 |
1714 | + initialDelay = 1.0 |
1715 | + factor = 1.6180339887498948 |
1716 | + |
1717 | + retryableHTTPCodes = [408, 500, 502, 503, 504] |
1718 | + |
1719 | + defaultTimeout = 300.0 |
1720 | + |
1721 | + debug = False |
1722 | + |
1723 | + |
1724 | + def __init__(self, url, tries=10, timeout=defaultTimeout, *args, **kwargs): |
1725 | + """ |
1726 | + Prepare the download information. |
1727 | + |
1728 | + Any additional positional or keyword arguments are passed on to |
1729 | + C{twisted.web.client.HTTPPageGetter}. |
1730 | + |
1731 | + @type url: C{nevow.url.URL} or C{unicode} or C{str} |
1732 | + @param url: The HTTP URL to attempt to download |
1733 | + |
1734 | + @type tries: C{int} |
1735 | + @param tries: The maximum number of retry attempts before giving up |
1736 | + |
1737 | + @type timeout: C{float} |
1738 | + @param timeout: Timeout value, in seconds, for the page fetch; |
1739 | + defaults to L{defaultTimeout} |
1740 | + |
1741 | + @param *args: Positional arguments to pass to the page getter, see |
1742 | + L{twisted.web.client.getPage}. |
1743 | + |
1744 | + @param **kwargs: Keyword arguments to pass to the page getter, see |
1745 | + L{twisted.web.client.getPage}. |
1746 | + """ |
1747 | + self.url = url |
1748 | + self.delay = self.initialDelay |
1749 | + self.tries = tries |
1750 | + self.timeout = timeout |
1751 | + self.args = args |
1752 | + self.kwargs = kwargs |
1753 | + |
1754 | + |
1755 | + def _writeLog(self, result, factory): |
1756 | + """ |
1757 | + Log an HTTP request or response. |
1758 | + """ |
1759 | + if factory.response_headers is None: |
1760 | + suffix = 'req' |
1761 | + headers = factory.headers |
1762 | + body = factory.postdata |
1763 | + else: |
1764 | + suffix = 'res' |
1765 | + headers = factory.response_headers |
1766 | + body = result |
1767 | + |
1768 | + path = urllib.splitquery(factory.path)[0] |
1769 | + path = '%s_%s_%s.log' % ( |
1770 | + time.time(), |
1771 | + path[1:].replace('/', '_').replace('.', '_'), |
1772 | + suffix) |
1773 | + fd = file(path, 'wb') |
1774 | + fd.write('%s %s\n' % (factory.method, factory.url)) |
1775 | + |
1776 | + if headers: |
1777 | + fd.write('\n') |
1778 | + for k, v in headers.iteritems(): |
1779 | + if not isinstance(v, list): |
1780 | + v = [v] |
1781 | + v = map(str, v) |
1782 | + fd.write('%s: %s\n' % (k, ';'.join(v))) |
1783 | + |
1784 | + fd.write('\n') |
1785 | + if body: |
1786 | + fd.write(body) |
1787 | + fd.close() |
1788 | + return result |
1789 | + |
1790 | + |
1791 | + def _handle206(self, f): |
1792 | + """ |
1793 | + Return any partial content when HTTP 206 is returned. |
1794 | + """ |
1795 | + f.trap(weberror.Error) |
1796 | + err = f.value |
1797 | + try: |
1798 | + if int(err.status) == http.PARTIAL_CONTENT: |
1799 | + return err.response |
1800 | + except (TypeError, ValueError): |
1801 | + pass |
1802 | + return f |
1803 | + |
1804 | + |
1805 | + def _getPage(self, *args, **kwargs): |
1806 | + """ |
1807 | + Fetch data from a URL over HTTP. |
1808 | + """ |
1809 | + kwargs.setdefault('agent', 'txConvore') |
1810 | + factory = client._makeGetterFactory( |
1811 | + self.url, |
1812 | + client.HTTPClientFactory, |
1813 | + timeout=self.timeout, |
1814 | + *args, **kwargs) |
1815 | + return factory.deferred, factory |
1816 | + |
1817 | + |
1818 | + def download(self): |
1819 | + """ |
1820 | + Download L{self.url}, retrying if necessary. |
1821 | + |
1822 | + @rtype: C{Deferred<response, headers>} |
1823 | + """ |
1824 | + d, f = self._getPage(*self.args, **self.kwargs) |
1825 | + if self.debug: |
1826 | + self._writeLog(None, f) |
1827 | + d.addCallback(self._writeLog, f) |
1828 | + d.addErrback(self._handle206) |
1829 | + d.addErrback(self.retryWeb) |
1830 | + d.addCallback(lambda data: (data, f.response_headers)) |
1831 | + return d |
1832 | + |
1833 | + |
1834 | + def retryWeb(self, f): |
1835 | + """ |
1836 | + Retry failed downloads in the case of "web errors." |
1837 | + |
1838 | + Only errors that are web related are considered for a retry attempt |
1839 | + and then only when the HTTP status code is one of those in |
1840 | + L{self.retryableHTTPCodes}. |
1841 | + |
1842 | + Other errors are not trapped. |
1843 | + """ |
1844 | + f.trap(weberror.Error, ineterror.ConnectionDone) |
1845 | + err = f.value |
1846 | + try: |
1847 | + if int(err.status) in self.retryableHTTPCodes: |
1848 | + return self.retry(f) |
1849 | + except (TypeError, ValueError): |
1850 | + pass |
1851 | + return f |
1852 | + |
1853 | + |
1854 | + def retry(self, f): |
1855 | + """ |
1856 | + The retry machinery. |
1857 | + |
1858 | + If C{self.tries} is greater than zero, a retry is attempted for |
1859 | + C{self.delay} seconds in the future. |
1860 | + """ |
1861 | + self.tries -= 1 |
1862 | + log.err(f, 'PerseverantDownloader is retrying, %d attempts left.' % ( |
1863 | + self.tries,)) |
1864 | + self.delay = min(self.delay * self.factor, self.maxDelay) |
1865 | + if self.tries == 0: |
1866 | + return f |
1867 | + return task.deferLater(reactor, self.delay, self.download) |