Merge lp:~james-w/canonical-identity-provider/captcha-timeline into lp:canonical-identity-provider/release
- captcha-timeline
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | James Westby |
Approved revision: | no longer in the source branch. |
Merged at revision: | 1029 |
Proposed branch: | lp:~james-w/canonical-identity-provider/captcha-timeline |
Merge into: | lp:canonical-identity-provider/release |
Diff against target: |
610 lines (+307/-32) 11 files modified
src/api/v10/handlers.py (+6/-2) src/api/v10/tests/test_handlers.py (+35/-2) src/api/v20/handlers.py (+7/-3) src/api/v20/tests/test_handlers.py (+44/-2) src/api/v20/tests/test_registration.py (+3/-2) src/identityprovider/models/captcha.py (+12/-7) src/identityprovider/tests/test_models_captcha.py (+30/-3) src/identityprovider/tests/test_timeline.py (+72/-0) src/identityprovider/timeline.py (+71/-0) src/webui/tests/test_views_ui.py (+23/-10) src/webui/views/ui.py (+4/-1) |
To merge this branch: | bzr merge lp:~james-w/canonical-identity-provider/captcha-timeline |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Ricardo Kirkner (community) | Approve | ||
Review via email: mp+181938@code.launchpad.net |
Commit message
Add entries to the request timeline when creating or verifying a captcha.
These http requests made during a request can take time, so record them
in the timeline. That way if there is a slow request involving one you
can see how long it took.
Description of the change
Hi,
This makes it so that timing information about recaptcha requests
will show up in oopses. The v1 api that makes these requests sometimes
times out, and the oops has no information about what is happening during
the 10s. This will make it such that you will see how long the request
to recaptcha took.
I didn't instrument the captcha verification in the v1 api as it wasn't
easy, and it is deprecated. If we see slowness in those requests we
can revisit that.
Thanks,
James
Ubuntu One Auto Pilot (otto-pilot) wrote : | # |
There are additional revisions which have not been approved in review. Please seek review and approval of these new revisions.
Preview Diff
1 | === modified file 'src/api/v10/handlers.py' |
2 | --- src/api/v10/handlers.py 2013-08-16 15:16:58 +0000 |
3 | +++ src/api/v10/handlers.py 2013-08-26 13:12:23 +0000 |
4 | @@ -38,6 +38,9 @@ |
5 | application_token_created, |
6 | application_token_invalidated, |
7 | ) |
8 | +from identityprovider.timeline import ( |
9 | + get_request_timing_function, |
10 | +) |
11 | |
12 | from oauth_backend.models import Token |
13 | |
14 | @@ -163,12 +166,13 @@ |
15 | |
16 | @named_operation |
17 | def new(self, request): |
18 | + timer = get_request_timing_function(request) |
19 | try: |
20 | - return Captcha.new().serialize() |
21 | + return Captcha.new(timer=timer).serialize() |
22 | except NewCaptchaError as e: |
23 | logger.exception("Failed to connect to reCaptcha server") |
24 | # TODO: Some better return here? |
25 | - return e.dummy |
26 | + return e.dummy.serialize() |
27 | |
28 | |
29 | class RegistrationHandler(LazrRestfulHandler): |
30 | |
31 | === modified file 'src/api/v10/tests/test_handlers.py' |
32 | --- src/api/v10/tests/test_handlers.py 2013-08-20 17:47:58 +0000 |
33 | +++ src/api/v10/tests/test_handlers.py 2013-08-26 13:12:23 +0000 |
34 | @@ -6,10 +6,14 @@ |
35 | from django.utils.http import urlencode |
36 | from django.core.urlresolvers import reverse |
37 | |
38 | -from mock import patch |
39 | +from mock import ( |
40 | + Mock, |
41 | + patch, |
42 | +) |
43 | from lazr.restfulclient.errors import HTTPError |
44 | |
45 | from gargoyle import gargoyle |
46 | +from timeline import Timeline |
47 | |
48 | from identityprovider.tests.utils import SSOBaseTestCase |
49 | from identityprovider.models import ( |
50 | @@ -17,6 +21,9 @@ |
51 | APIUser, |
52 | EmailAddress, |
53 | ) |
54 | +from identityprovider.models.captcha import ( |
55 | + Captcha, |
56 | +) |
57 | from identityprovider.models.const import ( |
58 | AccountStatus, |
59 | EmailStatus, |
60 | @@ -29,7 +36,10 @@ |
61 | from identityprovider.tests.utils import authorization_header_from_token |
62 | from identityprovider.validators import PASSWORD_POLICY_ERROR |
63 | |
64 | -from api.v10.handlers import AuthenticationHandler |
65 | +from api.v10.handlers import ( |
66 | + AuthenticationHandler, |
67 | + CaptchaHandler, |
68 | +) |
69 | from api.v10.tests.utils import ( |
70 | AnonAPITestCase, |
71 | MockRequest, |
72 | @@ -548,3 +558,26 @@ |
73 | self.assertContains(response, email.email) |
74 | updatedemail = EmailAddress.objects.get(email=email.email) |
75 | self.assertEqual(EmailStatus.VALIDATED, updatedemail.status) |
76 | + |
77 | + |
78 | +class CaptchasTestCase(SSOBaseTestCase): |
79 | + |
80 | + url = reverse('api-10-captchas') |
81 | + |
82 | + @patch.object(Captcha, '_open') |
83 | + def test_returns_dummy(self, mock_open): |
84 | + resp = self.client.post(self.url, {'ws.op': 'new'}, |
85 | + HTTP_ACCEPT='application/json') |
86 | + self.assertEqual(None, json.loads(resp.content)['image_url']) |
87 | + |
88 | + @patch.object(Captcha, '_open') |
89 | + def test_puts_entry_in_timeline(self, mock_open): |
90 | + handler = CaptchaHandler() |
91 | + request = Mock() |
92 | + timeline = Timeline() |
93 | + request.META = {'timeline.timeline': timeline} |
94 | + handler.new(request) |
95 | + self.assertEqual(1, len(timeline.actions)) |
96 | + self.assertEqual('captcha-new', timeline.actions[0].category) |
97 | + self.assertEqual(mock_open.call_args[0][0], |
98 | + timeline.actions[0].detail) |
99 | |
100 | === modified file 'src/api/v20/handlers.py' |
101 | --- src/api/v20/handlers.py 2013-08-20 17:47:58 +0000 |
102 | +++ src/api/v20/handlers.py 2013-08-26 13:12:23 +0000 |
103 | @@ -43,8 +43,9 @@ |
104 | AccountStatus, |
105 | TokenType, |
106 | ) |
107 | +from identityprovider.store import SSODataStore |
108 | +from identityprovider.timeline import get_request_timing_function |
109 | from identityprovider.utils import redirection_url_for_token |
110 | -from identityprovider.store import SSODataStore |
111 | from webui.decorators import limitlogin |
112 | |
113 | |
114 | @@ -164,8 +165,9 @@ |
115 | gargoyle.is_active('CAPTCHA')): |
116 | extra = {} |
117 | if data.get('create_captcha', True): |
118 | + timer = get_request_timing_function(request) |
119 | try: |
120 | - extra = Captcha.new().serialize() |
121 | + extra = Captcha.new(timer=timer).serialize() |
122 | except Exception: |
123 | logger.exception('failed to create reCaptcha') |
124 | return errors.CAPTCHA_REQUIRED(**extra) |
125 | @@ -175,8 +177,10 @@ |
126 | captcha = Captcha(captcha_id) |
127 | verified = False |
128 | try: |
129 | + timer = get_request_timing_function(request) |
130 | verified = captcha.verify( |
131 | - captcha_solution, remote_addr, data['email']) |
132 | + captcha_solution, remote_addr, data['email'], |
133 | + timer=timer) |
134 | except VerifyCaptchaError as e: |
135 | # only expose HTTP error codes to client |
136 | code = e.response.code if e.response.code > 200 else None |
137 | |
138 | === modified file 'src/api/v20/tests/test_handlers.py' |
139 | --- src/api/v20/tests/test_handlers.py 2013-08-20 18:02:58 +0000 |
140 | +++ src/api/v20/tests/test_handlers.py 2013-08-26 13:12:23 +0000 |
141 | @@ -1,20 +1,22 @@ |
142 | import json |
143 | - |
144 | +from StringIO import StringIO |
145 | from urllib import urlencode |
146 | |
147 | from django.core import mail |
148 | from django.core.serializers.json import DjangoJSONEncoder |
149 | from django.core.urlresolvers import NoReverseMatch, reverse |
150 | from gargoyle.testutils import switches |
151 | -from mock import ANY, patch |
152 | +from mock import ANY, Mock, patch |
153 | from oauth_backend.models import Token |
154 | from oauth.oauth import OAuthRequest |
155 | +from timeline import Timeline |
156 | |
157 | from identityprovider.models import ( |
158 | Account, |
159 | AuthToken, |
160 | EmailAddress, |
161 | ) |
162 | +from identityprovider.models.captcha import Captcha, CaptchaResponse |
163 | from identityprovider.models.const import ( |
164 | AccountStatus, |
165 | EmailStatus, |
166 | @@ -812,3 +814,43 @@ |
167 | url=url, token=self.token, method='DELETE') |
168 | |
169 | self.assertEqual(response.status_code, 404) |
170 | + |
171 | + |
172 | +class AccountRegistrationHandlerTests(BaseTestCase): |
173 | + |
174 | + @patch.object(Captcha, '_open') |
175 | + @switches(CAPTCHA=True) |
176 | + def test_timeline_for_captcha_generation(self, mock_open): |
177 | + handler = handlers.AccountRegistrationHandler() |
178 | + request = Mock() |
179 | + timeline = Timeline() |
180 | + request.META = { |
181 | + 'timeline.timeline': timeline, |
182 | + 'CONTENT_TYPE': 'application/json', |
183 | + } |
184 | + request.data = dict(create_captcha=True) |
185 | + handler.create(request) |
186 | + self.assertEqual(1, len(timeline.actions)) |
187 | + self.assertEqual('captcha-new', timeline.actions[0].category) |
188 | + self.assertEqual(mock_open.call_args[0][0], |
189 | + timeline.actions[0].detail) |
190 | + |
191 | + @patch.object(Captcha, '_open') |
192 | + @switches(CAPTCHA=True) |
193 | + def test_timeline_for_captcha_verification(self, mock_open): |
194 | + mock_open.response = CaptchaResponse(500, StringIO('')) |
195 | + handler = handlers.AccountRegistrationHandler() |
196 | + request = Mock() |
197 | + timeline = Timeline() |
198 | + request.META = { |
199 | + 'timeline.timeline': timeline, |
200 | + 'CONTENT_TYPE': 'application/json', |
201 | + } |
202 | + request.environ = {'REMOTE_ADDR': '127.0.0.1'} |
203 | + request.data = dict(captcha_id='captcha-id', |
204 | + captcha_solution='captcha_solution', |
205 | + email='test@test.com') |
206 | + with patch_settings(DISABLE_CAPTCHA_VERIFICATION=False): |
207 | + handler.create(request) |
208 | + self.assertEqual(1, len(timeline.actions)) |
209 | + self.assertEqual('captcha-verify', timeline.actions[0].category) |
210 | |
211 | === modified file 'src/api/v20/tests/test_registration.py' |
212 | --- src/api/v20/tests/test_registration.py 2013-08-08 20:17:55 +0000 |
213 | +++ src/api/v20/tests/test_registration.py 2013-08-26 13:12:23 +0000 |
214 | @@ -1,4 +1,5 @@ |
215 | from mock import ( |
216 | + ANY, |
217 | patch, |
218 | call as mock_call, |
219 | ) |
220 | @@ -177,7 +178,7 @@ |
221 | |
222 | self.assertEqual(response.status_code, 201) |
223 | expected_calls = mock_call('999').verify( |
224 | - 'foo bar', '127.0.0.1', self.data['email']).call_list() |
225 | + 'foo bar', '127.0.0.1', self.data['email'], timer=ANY).call_list() |
226 | self.assertEqual(mock_captcha.mock_calls, expected_calls) |
227 | |
228 | self.assertIn('openid', json_body) |
229 | @@ -197,7 +198,7 @@ |
230 | response, json_body = call(self.handler.create, self.url, self.data) |
231 | self.assertEqual(response.status_code, 403) |
232 | expected_calls = mock_call('999').verify( |
233 | - 'foo bar', '127.0.0.1', self.data['email']).call_list() |
234 | + 'foo bar', '127.0.0.1', self.data['email'], timer=ANY).call_list() |
235 | self.assertEqual(mock_captcha.mock_calls, expected_calls) |
236 | |
237 | self.assertEqual(json_body['code'], "CAPTCHA_FAILURE") |
238 | |
239 | === modified file 'src/identityprovider/models/captcha.py' |
240 | --- src/identityprovider/models/captcha.py 2013-05-21 13:57:39 +0000 |
241 | +++ src/identityprovider/models/captcha.py 2013-08-26 13:12:23 +0000 |
242 | @@ -9,6 +9,8 @@ |
243 | |
244 | from django.conf import settings |
245 | |
246 | +from identityprovider.timeline import maybe_contextmanager |
247 | + |
248 | |
249 | class CaptchaError(Exception): |
250 | """All instances of CaptchaError have a traceback member.""" |
251 | @@ -122,15 +124,16 @@ |
252 | } |
253 | |
254 | @classmethod |
255 | - def new(cls): |
256 | + def new(cls, timer=None): |
257 | cls._setup_opener() |
258 | url = (settings.CAPTCHA_API_URL + |
259 | '/challenge?k=%s' % settings.CAPTCHA_PUBLIC_KEY) |
260 | - response = cls._open(url) |
261 | - if response.is_error: |
262 | - raise NewCaptchaError(response.traceback, cls(None, None)) |
263 | + with maybe_contextmanager(timer, 'captcha-new', url): |
264 | + response = cls._open(url) |
265 | + if response.is_error: |
266 | + raise NewCaptchaError(response.traceback, cls(None, None)) |
267 | |
268 | - data = response.data() |
269 | + data = response.data() |
270 | m = re.search(r"challenge\s*:\s*'(.+?)'", data, re.M | re.S) |
271 | if m: |
272 | captcha_id = m.group(1) |
273 | @@ -173,7 +176,7 @@ |
274 | |
275 | return CaptchaResponse(response.code, response, None) |
276 | |
277 | - def verify(self, captcha_solution, remote_ip, email): |
278 | + def verify(self, captcha_solution, remote_ip, email, timer=None): |
279 | if self._verified is not None: |
280 | return self._verified |
281 | |
282 | @@ -194,7 +197,9 @@ |
283 | 'response': captcha_solution, |
284 | }) |
285 | request = urllib2.Request(settings.CAPTCHA_VERIFY_URL, request_data) |
286 | - self.response = self._open(request) |
287 | + with maybe_contextmanager(timer, 'captcha-verify', |
288 | + settings.CAPTCHA_VERIFY_URL): |
289 | + self.response = self._open(request) |
290 | |
291 | if not self.response.is_error: |
292 | response_data = self.response.data() |
293 | |
294 | === modified file 'src/identityprovider/tests/test_models_captcha.py' |
295 | --- src/identityprovider/tests/test_models_captcha.py 2013-03-01 20:26:19 +0000 |
296 | +++ src/identityprovider/tests/test_models_captcha.py 2013-08-26 13:12:23 +0000 |
297 | @@ -3,21 +3,21 @@ |
298 | |
299 | import unittest |
300 | import urllib2 |
301 | +from contextlib import contextmanager |
302 | +from cStringIO import StringIO |
303 | |
304 | +from django.conf import settings |
305 | from mock import ( |
306 | Mock, |
307 | patch, |
308 | ) |
309 | |
310 | -from cStringIO import StringIO |
311 | - |
312 | from identityprovider.models.captcha import ( |
313 | Captcha, |
314 | CaptchaResponse, |
315 | NewCaptchaError, |
316 | VerifyCaptchaError, |
317 | ) |
318 | - |
319 | from identityprovider.tests.utils import patch_settings |
320 | |
321 | |
322 | @@ -66,6 +66,20 @@ |
323 | |
324 | self.assertRaises(urllib2.URLError, Captcha.new) |
325 | |
326 | + @patch.object(Captcha, '_open') |
327 | + def test_new_captcha_uses_timer(self, mock_open): |
328 | + passed_info = [] |
329 | + |
330 | + @contextmanager |
331 | + def timer_fn(category, detail): |
332 | + passed_info.append((category, detail)) |
333 | + yield |
334 | + mock_open.return_value = CaptchaResponse(200, StringIO('')) |
335 | + Captcha.new(timer=timer_fn) |
336 | + self.assertEqual(1, len(passed_info)) |
337 | + self.assertEqual('captcha-new', passed_info[0][0]) |
338 | + self.assertEqual(mock_open.call_args[0][0], passed_info[0][1]) |
339 | + |
340 | |
341 | class CaptchaVerifyTestCase(unittest.TestCase): |
342 | |
343 | @@ -157,3 +171,16 @@ |
344 | self.assertRaises( |
345 | VerifyCaptchaError, c.verify, None, 'localhost', '' |
346 | ) |
347 | + |
348 | + @patch.object(Captcha, '_open') |
349 | + def test_verify_uses_timer(self, mock_open): |
350 | + passed_info = [] |
351 | + |
352 | + @contextmanager |
353 | + def timer_fn(category, detail): |
354 | + passed_info.append((category, detail)) |
355 | + yield |
356 | + c = Captcha(None) |
357 | + c.verify(None, 'localhost', '', timer=timer_fn) |
358 | + self.assertEqual('captcha-verify', passed_info[0][0]) |
359 | + self.assertEqual(settings.CAPTCHA_VERIFY_URL, passed_info[0][1]) |
360 | |
361 | === added file 'src/identityprovider/tests/test_timeline.py' |
362 | --- src/identityprovider/tests/test_timeline.py 1970-01-01 00:00:00 +0000 |
363 | +++ src/identityprovider/tests/test_timeline.py 2013-08-26 13:12:23 +0000 |
364 | @@ -0,0 +1,72 @@ |
365 | +from contextlib import contextmanager |
366 | +from unittest import TestCase |
367 | + |
368 | +from mock import Mock |
369 | +from timeline import Timeline |
370 | + |
371 | +from identityprovider.timeline import ( |
372 | + get_request_timeline, |
373 | + get_timing_function, |
374 | + maybe_contextmanager, |
375 | +) |
376 | + |
377 | + |
378 | +class GetRequestTimelineTests(TestCase): |
379 | + |
380 | + def test_no_timeline(self): |
381 | + request = Mock() |
382 | + request.META = dict() |
383 | + self.assertEqual(None, get_request_timeline(request)) |
384 | + |
385 | + def test_timeline(self): |
386 | + request = Mock() |
387 | + timeline = object() |
388 | + request.META = {'timeline.timeline': timeline} |
389 | + self.assertEqual(timeline, get_request_timeline(request)) |
390 | + |
391 | + |
392 | +class GetTimingFunctionTests(TestCase): |
393 | + |
394 | + def test_no_timeline(self): |
395 | + timer_fn = get_timing_function(None) |
396 | + with timer_fn('category', 'detail'): |
397 | + # No crash |
398 | + pass |
399 | + |
400 | + def test_timeline(self): |
401 | + timeline = Timeline() |
402 | + timer_fn = get_timing_function(timeline) |
403 | + with timer_fn('category', 'detail'): |
404 | + pass |
405 | + self.assertEqual(1, len(timeline.actions)) |
406 | + self.assertEqual('category', timeline.actions[0].category) |
407 | + self.assertEqual('detail', timeline.actions[0].detail) |
408 | + |
409 | + |
410 | +class MaybeContextManagerTests(TestCase): |
411 | + |
412 | + def test_None(self): |
413 | + with maybe_contextmanager(None, 'a'): |
414 | + # no crash |
415 | + pass |
416 | + |
417 | + def test_passes_args(self): |
418 | + passed_args = [] |
419 | + passed_kwargs = [] |
420 | + |
421 | + @contextmanager |
422 | + def cm(*args, **kwargs): |
423 | + passed_args.append(args) |
424 | + passed_kwargs.append(kwargs) |
425 | + yield |
426 | + with maybe_contextmanager(cm, 'a', b='c'): |
427 | + pass |
428 | + self.assertEqual([('a',)], passed_args) |
429 | + self.assertEqual([dict(b='c')], passed_kwargs) |
430 | + |
431 | + def test_yields_value(self): |
432 | + @contextmanager |
433 | + def cm(*args, **kwargs): |
434 | + yield 'the value' |
435 | + with maybe_contextmanager(cm) as the_value: |
436 | + self.assertEqual('the value', the_value) |
437 | |
438 | === added file 'src/identityprovider/timeline.py' |
439 | --- src/identityprovider/timeline.py 1970-01-01 00:00:00 +0000 |
440 | +++ src/identityprovider/timeline.py 2013-08-26 13:12:23 +0000 |
441 | @@ -0,0 +1,71 @@ |
442 | +from contextlib import contextmanager |
443 | + |
444 | + |
445 | +def get_request_timeline(request): |
446 | + """Get the timeline for a request, if any. |
447 | + |
448 | + :return: a Timeline instance if the request has one, or None if not. |
449 | + """ |
450 | + return request.META.get('timeline.timeline', None) |
451 | + |
452 | + |
453 | +def get_timing_function(timeline): |
454 | + """Get a context manager that will put timing info in to a timeline. |
455 | + |
456 | + Given |
457 | + |
458 | + timer_fn = get_timing_function(timeline, detail_fn) |
459 | + |
460 | + def request(method, url): |
461 | + with timer_fn('http-{}'.format(method), url): |
462 | + <..make request..> |
463 | + |
464 | + calling request('GET', 'http://google.com/') will lead to an action |
465 | + being inserted in to the timeline with |
466 | + |
467 | + start: time at which "with timer_fn(..." was exectuted. |
468 | + duration: duration until the with block was left, i.e. duration of |
469 | + the request. |
470 | + category: 'http-GET' |
471 | + detail: 'http://google.com/' |
472 | + |
473 | + :param timeline: the Timeline instance in which to create the action, |
474 | + or None to take no timing information. |
475 | + :return: a context manager that can be called with info about the |
476 | + operation, and will record an action on the timeline for the duration |
477 | + between __enter__ and __exit__. |
478 | + """ |
479 | + @contextmanager |
480 | + def timer_fn(category, detail): |
481 | + if not timeline: |
482 | + yield |
483 | + else: |
484 | + action = timeline.start(category, detail) |
485 | + yield |
486 | + action.finish() |
487 | + return timer_fn |
488 | + |
489 | + |
490 | +def get_request_timing_function(request): |
491 | + return get_timing_function(get_request_timeline(request)) |
492 | + |
493 | + |
494 | +@contextmanager |
495 | +def maybe_contextmanager(cm, *args, **kwargs): |
496 | + """Allow an optional context manager to be used. |
497 | + |
498 | + When you take an optional context manager as an argument, it can be |
499 | + tricky to use it, as you can't do "with None:". |
500 | + |
501 | + This function allows for you to call |
502 | + |
503 | + with maybe_contextmanager(cm, args, ...): |
504 | + pass |
505 | + |
506 | + and it will work whether cm is a contextmanager or None. |
507 | + """ |
508 | + if cm is None: |
509 | + yield |
510 | + else: |
511 | + with cm(*args, **kwargs) as info: |
512 | + yield info |
513 | |
514 | === modified file 'src/webui/tests/test_views_ui.py' |
515 | --- src/webui/tests/test_views_ui.py 2013-08-12 14:33:47 +0000 |
516 | +++ src/webui/tests/test_views_ui.py 2013-08-26 13:12:23 +0000 |
517 | @@ -6,24 +6,22 @@ |
518 | import re |
519 | import urllib |
520 | import urllib2 |
521 | - |
522 | from functools import partial |
523 | +from StringIO import StringIO |
524 | +from unittest import skipUnless |
525 | from urlparse import urlsplit |
526 | |
527 | -from mock import patch, Mock |
528 | -from pyquery import PyQuery |
529 | -from unittest import skipUnless |
530 | - |
531 | from django.conf import settings |
532 | from django.contrib.sessions.models import Session |
533 | from django.core import mail |
534 | from django.core.urlresolvers import reverse |
535 | from django.http import QueryDict, HttpResponse |
536 | - |
537 | +from gargoyle import gargoyle |
538 | from gargoyle.testutils import switches |
539 | -from gargoyle import gargoyle |
540 | - |
541 | +from mock import patch, Mock |
542 | from openid.message import IDENTIFIER_SELECT |
543 | +from pyquery import PyQuery |
544 | +from timeline import Timeline |
545 | from u1testutils.logging import LogHandlerTestCase |
546 | |
547 | from identityprovider import signed |
548 | @@ -41,7 +39,7 @@ |
549 | twofactor, |
550 | ) |
551 | from identityprovider.models.authtoken import create_token |
552 | -from identityprovider.models.captcha import Captcha |
553 | +from identityprovider.models.captcha import Captcha, CaptchaResponse |
554 | from identityprovider.models.const import AccountStatus, EmailStatus, TokenType |
555 | from identityprovider.readonly import ReadOnlyManager |
556 | from identityprovider.tests import DEFAULT_USER_PASSWORD |
557 | @@ -52,7 +50,6 @@ |
558 | ) |
559 | from identityprovider.utils import validate_launchpad_password |
560 | from identityprovider.views import server |
561 | - |
562 | from webui import decorators |
563 | from webui.views import ui |
564 | |
565 | @@ -138,6 +135,22 @@ |
566 | return r |
567 | |
568 | |
569 | +class VerifyCaptureResponseTests(SSOBaseTestCase): |
570 | + |
571 | + @patch.object(Captcha, '_open') |
572 | + def test_uses_timeline_from_request(self, mock_open): |
573 | + mock_open.return_value = CaptchaResponse(200, StringIO('true\na')) |
574 | + request = Mock() |
575 | + timeline = Timeline() |
576 | + request.META = {'timeline.timeline': timeline} |
577 | + request.POST = {'recaptcha_challenge_field': 'captcha-id'} |
578 | + request.environ = {'REMOTE_ADDR': '127.0.0.1'} |
579 | + with patch.multiple(settings, DISABLE_CAPTCHA_VERIFICATION=False): |
580 | + ui._verify_captcha_response(None, request, None) |
581 | + self.assertEqual(1, len(timeline.actions)) |
582 | + self.assertEqual('captcha-verify', timeline.actions[0].category) |
583 | + |
584 | + |
585 | class SpanishUIViewsTestCase(BaseTestCase): |
586 | |
587 | def setUp(self): |
588 | |
589 | === modified file 'src/webui/views/ui.py' |
590 | --- src/webui/views/ui.py 2013-08-21 15:13:22 +0000 |
591 | +++ src/webui/views/ui.py 2013-08-26 13:12:23 +0000 |
592 | @@ -74,6 +74,7 @@ |
593 | redirection_url_for_token, |
594 | ) |
595 | from identityprovider.stats import stats |
596 | +from identityprovider.timeline import get_request_timing_function |
597 | from identityprovider.views.utils import ( |
598 | get_rpconfig, |
599 | get_rpconfig_from_request, |
600 | @@ -679,7 +680,9 @@ |
601 | email = request.POST.get('email', '') |
602 | ip_addr = request.environ["REMOTE_ADDR"] |
603 | try: |
604 | - verified = captcha.verify(captcha_solution, ip_addr, email) |
605 | + timer_fn = get_request_timing_function(request) |
606 | + verified = captcha.verify(captcha_solution, ip_addr, email, |
607 | + timer=timer_fn) |
608 | if verified: |
609 | return None |
610 | except VerifyCaptchaError: |
LGTM, but if you allow for nitpicking... we have certain conventions for import grouping and ordering; this is
1. group imports by origin (stdlib, 3rd party, app)
2. within each group, subgroup by 'import ... ' then 'from ... import ...'
3. within each subgroup, order asciibetically
l. 109 should go before l. 107
l. 298-299: according to the convention, l. 298 should come after the 'import ..' statements, and l.299 shoudl go into a different group
l. 347-348: these should go into a second group
l. 501: this should go after the 'import ..' lines