Merge lp:~cjwatson/canonical-identity-provider/readonly-requests into lp:canonical-identity-provider/release

Proposed by Colin Watson
Status: Merged
Approved by: Colin Watson
Approved revision: no longer in the source branch.
Merge reported by: Otto Co-Pilot
Merged at revision: not available
Proposed branch: lp:~cjwatson/canonical-identity-provider/readonly-requests
Merge into: lp:canonical-identity-provider/release
Diff against target: 421 lines (+88/-94)
8 files modified
requirements.txt (+1/-0)
requirements_devel.txt (+3/-1)
src/api/v10/tests/utils.py (+15/-8)
src/identityprovider/readonly.py (+4/-11)
src/identityprovider/tests/test_command_readonly.py (+5/-3)
src/identityprovider/tests/test_readonly.py (+52/-69)
src/identityprovider/tests/test_wsgi.py (+1/-2)
src/identityprovider/tests/utils.py (+7/-0)
To merge this branch: bzr merge lp:~cjwatson/canonical-identity-provider/readonly-requests
Reviewer Review Type Date Requested Status
Adam Collard (community) Approve
Review via email: mp+336739@code.launchpad.net

Commit message

Convert identityprovider.readonly to requests, which is easier to test.

Description of the change

While working out what would be involved in a Python 3 port, I ran across the urllib2 mocks here, which are ugly and a bit difficult to make 2/3 bilingual, so I thought it'd be best to just port this code to requests.

Incidentally, the old code was buggy: it doesn't restore the previous default socket timeout if urlopen raises an exception. Who knows if this ever caused a problem ...

I had to upgrade wsgi-intercept to support requests. u1testutils.wsgi_intercept isn't friends with the new version, but it seems like unnecessary complexity anyway, so I just open-coded the few lines that we need.

Requires https://code.launchpad.net/~cjwatson/canonical-identity-provider/dependencies-readonly-requests/+merge/336738.

To post a comment you must log in.
Revision history for this message
Adam Collard (adam-collard) wrote :

Nice, responses is much easier to read.

review: Approve
Revision history for this message
Otto Co-Pilot (otto-copilot) wrote :
Revision history for this message
Otto Co-Pilot (otto-copilot) wrote :

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'requirements.txt'
2--- requirements.txt 2017-04-03 17:28:42 +0000
3+++ requirements.txt 2018-01-29 17:03:41 +0000
4@@ -31,6 +31,7 @@
5 pycparser==2.14
6 pymacaroons==0.9.1
7 raven==6.0.0
8+requests==2.10.0
9 requests-oauthlib==0.4.2
10 six==1.10.0
11 statsd==3.1
12
13=== modified file 'requirements_devel.txt'
14--- requirements_devel.txt 2016-07-06 00:29:59 +0000
15+++ requirements_devel.txt 2018-01-29 17:03:41 +0000
16@@ -1,5 +1,6 @@
17 -r requirements.txt
18
19+cookies==2.2.1
20 coverage==3.6
21 cssselect==0.7.1
22 ecdsa==0.11
23@@ -19,6 +20,7 @@
24 pyquery==1.2.1
25 python-mimeparse==0.1.4
26 python-subunit==1.1.0
27+responses==0.8.1
28 selenium==2.53.6
29 sqlparse==0.1.4
30 sst==0.2.5dev
31@@ -26,6 +28,6 @@
32 testtools==0.9.39
33 txfixtures==0.1.4
34 u1-test-utils==0.5
35-wsgi-intercept==0.5.1
36+wsgi-intercept==1.6.0
37 -r requirements_docs.txt
38 pygpgme==0.3
39
40=== modified file 'src/api/v10/tests/utils.py'
41--- src/api/v10/tests/utils.py 2015-11-24 16:30:04 +0000
42+++ src/api/v10/tests/utils.py 2018-01-29 17:03:41 +0000
43@@ -10,7 +10,12 @@
44 from django.core.handlers.wsgi import WSGIHandler
45 from django.test.utils import override_settings
46 from lazr.restfulclient.resource import ServiceRoot
47-from u1testutils.wsgi_intercept import WSGIInterceptedTestCase
48+from wsgi_intercept import (
49+ add_wsgi_intercept,
50+ httplib2_intercept,
51+ remove_wsgi_intercept,
52+ urllib_intercept,
53+)
54
55 from identityprovider.tests.utils import SSOBaseTransactionTestCase
56 from identityprovider.tests.factory import SSOObjectFactory
57@@ -22,19 +27,21 @@
58
59
60 @override_settings(CAPTCHA_PUBLIC_KEY='public', CAPTCHA_PRIVATE_KEY='private')
61-class AnonAPITestCase(SSOBaseTransactionTestCase, WSGIInterceptedTestCase):
62+class AnonAPITestCase(SSOBaseTransactionTestCase):
63 factory = SSOObjectFactory()
64
65 def setUp(self):
66 super(AnonAPITestCase, self).setUp()
67 host_url = settings.SSO_ROOT_URL.rstrip('/')
68 parse = urlparse(host_url)
69- callbacks = {
70- 'default': WSGIHandler,
71- (parse.hostname, parse.port): WSGIHandler,
72- }
73- self.setup_intercept(callbacks, intercept_api=True)
74- self.addCleanup(self.teardown_intercept)
75+ self.unset_env('http_proxy')
76+ self.unset_env('https_proxy')
77+ urllib_intercept.install_opener()
78+ self.addCleanup(urllib_intercept.uninstall_opener)
79+ httplib2_intercept.install()
80+ self.addCleanup(httplib2_intercept.uninstall)
81+ add_wsgi_intercept(parse.hostname, parse.port, WSGIHandler)
82+ self.addCleanup(remove_wsgi_intercept, parse.hostname, parse.port)
83 self.api = ServiceRoot(None, host_url + '/api/1.0')
84
85 # patch Captcha so it never hits the real network
86
87=== modified file 'src/identityprovider/readonly.py'
88--- src/identityprovider/readonly.py 2013-06-20 15:37:44 +0000
89+++ src/identityprovider/readonly.py 2018-01-29 17:03:41 +0000
90@@ -3,11 +3,9 @@
91
92 import json
93 import os
94-import socket
95 import stat
96-import urllib
97-import urllib2
98
99+import requests
100 from django.conf import settings
101
102 # When a request arrives, the middleware will:
103@@ -54,7 +52,6 @@
104 if post is None:
105 post = []
106 post.append(('secret', settings.READONLY_SECRET))
107- post = urllib.urlencode(post)
108
109 if scheme is None:
110 scheme = 'http'
111@@ -66,14 +63,10 @@
112 headers = {}
113 if virtual_host is not None:
114 headers['Host'] = virtual_host
115- req = urllib2.Request(url, headers=headers, data=post)
116 try:
117- oldtimeout = socket.getdefaulttimeout()
118- socket.setdefaulttimeout(5)
119- datafile = urllib2.urlopen(req)
120- data = datafile.read()
121- socket.setdefaulttimeout(oldtimeout)
122- except urllib2.URLError:
123+ response = requests.post(url, headers=headers, data=post, timeout=5)
124+ data = response.content
125+ except requests.RequestException:
126 data = None
127 return data
128
129
130=== modified file 'src/identityprovider/tests/test_command_readonly.py'
131--- src/identityprovider/tests/test_command_readonly.py 2018-01-29 01:46:19 +0000
132+++ src/identityprovider/tests/test_command_readonly.py 2018-01-29 17:03:41 +0000
133@@ -12,7 +12,7 @@
134 from wsgi_intercept import (
135 add_wsgi_intercept,
136 remove_wsgi_intercept,
137- urllib2_intercept,
138+ requests_intercept,
139 )
140
141 from identityprovider.readonly import ReadOnlyManager
142@@ -46,8 +46,10 @@
143 _DBFAILOVER_FLAG_DIR)
144
145 # setup wsgi intercept mechanism to simulate wsgi server
146- urllib2_intercept.install_opener()
147- self.addCleanup(urllib2_intercept.uninstall_opener)
148+ self.unset_env('http_proxy')
149+ self.unset_env('https_proxy')
150+ requests_intercept.install()
151+ self.addCleanup(requests_intercept.uninstall)
152 for server in self.servers:
153 add_wsgi_intercept(server['HOST'], int(server['PORT']),
154 WSGIHandler)
155
156=== modified file 'src/identityprovider/tests/test_readonly.py'
157--- src/identityprovider/tests/test_readonly.py 2015-11-24 16:47:21 +0000
158+++ src/identityprovider/tests/test_readonly.py 2018-01-29 17:03:41 +0000
159@@ -6,11 +6,9 @@
160 import stat
161 import shutil
162 import tempfile
163-import socket
164-import urllib2
165-
166-from StringIO import StringIO
167-
168+
169+import requests
170+import responses
171 from django.conf import settings
172 from django.core.urlresolvers import reverse
173
174@@ -50,55 +48,49 @@
175 scheme = 'https'
176 vhost = 'http://foobar.baz'
177
178- def setup_mock_urlopen(self, func):
179- self.orig_urlopen = urllib2.urlopen
180- urllib2.urlopen = func
181-
182- def restore_orig_urlopen(self):
183- urllib2.urlopen = self.orig_urlopen
184-
185- def mock_urlopen(self, req, data=None):
186- self.req = req
187- self.assertEqual(5, socket.getdefaulttimeout())
188- return StringIO(self.msg)
189-
190+ @responses.activate
191 def test_plain_remote_req(self):
192- self.setup_mock_urlopen(self.mock_urlopen)
193+ responses.add(
194+ responses.POST, 'http://%s/readonlydata' % self.host,
195+ body=self.msg)
196 server = {'host': self.host}
197 self.assertEqual(self.msg, _remote_req(**server))
198- self.assertEqual(self.host, self.req.get_host())
199- self.restore_orig_urlopen()
200+ # This tests the PreparedRequest, which is before urllib3 inserts a
201+ # default Host header.
202+ self.assertNotIn('Host', responses.calls[0].request.headers)
203
204+ @responses.activate
205 def test_https_remote_req(self):
206- self.setup_mock_urlopen(self.mock_urlopen)
207+ responses.add(
208+ responses.POST, '%s://%s/readonlydata' % (self.scheme, self.host),
209+ body=self.msg)
210 server = {'host': self.host, 'scheme': self.scheme}
211 self.assertEqual(self.msg, _remote_req(**server))
212- self.assertEqual(self.scheme, self.req.get_type())
213- self.assertEqual(self.host, self.req.get_host())
214- self.restore_orig_urlopen()
215+ # This tests the PreparedRequest, which is before urllib3 inserts a
216+ # default Host header.
217+ self.assertNotIn('Host', responses.calls[0].request.headers)
218
219+ @responses.activate
220 def test_vhost_remote_req(self):
221- self.setup_mock_urlopen(self.mock_urlopen)
222+ responses.add(
223+ responses.POST, '%s://%s/readonlydata' % (self.scheme, self.host),
224+ body=self.msg)
225 server = {
226 'host': self.host,
227 'scheme': self.scheme,
228 'virtual_host': self.vhost,
229 }
230 self.assertEqual(self.msg, _remote_req(**server))
231- headers = {'Host': self.vhost}
232- self.assertEqual(headers, self.req.headers)
233- self.assertEqual(self.scheme, self.req.get_type())
234- self.assertEqual(self.host, self.req.get_host())
235- self.restore_orig_urlopen()
236-
237- def test_remote_req_urllib2_error(self):
238- def mock_urlopen(req, data=None):
239- raise urllib2.URLError((-1, 'error'))
240-
241- self.setup_mock_urlopen(mock_urlopen)
242+ self.assertEqual(
243+ self.vhost, responses.calls[0].request.headers['Host'])
244+
245+ @responses.activate
246+ def test_remote_req_error(self):
247+ responses.add(
248+ responses.POST, 'http://%s/readonlydata' % self.host,
249+ body=requests.RequestException('error'))
250 server = {'host': self.host}
251 self.assertEqual(None, _remote_req(**server))
252- self.restore_orig_urlopen()
253
254
255 class ReadOnlyFlagFilesTestCase(ReadOnlyBaseTestCase):
256@@ -179,18 +171,6 @@
257 assert self.client.login(
258 username=account.preferredemail.email, password='password')
259
260- def patch_urlopen(self, mock_urlopen=None):
261- self.reqs = []
262-
263- def _mock_urlopen(req, data=None):
264- self.reqs.append(req)
265- return StringIO(json.dumps({}))
266-
267- if mock_urlopen is None:
268- mock_urlopen = _mock_urlopen
269-
270- self.patch(urllib2, 'urlopen', new=mock_urlopen)
271-
272 def test_readonly_admin(self):
273 self.login_with_staff()
274
275@@ -219,13 +199,10 @@
276 atts = get_server_atts(servers)
277 self.assertEqual(atts, expected)
278
279+ @responses.activate
280 def test_get_server_atts_data_error(self):
281-
282- def mock_loads(data):
283- raise ValueError()
284-
285- self.patch(json, 'loads', new=mock_loads)
286- self.patch_urlopen()
287+ responses.add(
288+ responses.POST, 'http://localhost:8000/readonlydata', body='{')
289
290 servers = [{'SERVER_ID': 'localhost', 'SCHEME': 'http',
291 'HOST': 'localhost', 'VIRTUAL_HOST': '', 'PORT': '8000'}]
292@@ -235,13 +212,11 @@
293 atts = get_server_atts(servers)
294 self.assertEqual(atts, expected)
295
296+ @responses.activate
297 def test_get_server_atts_readonly(self):
298-
299- def mock_urlopen(req, data=None):
300- data = {'readonly': True}
301- return StringIO(json.dumps(data))
302-
303- self.patch_urlopen(mock_urlopen=mock_urlopen)
304+ responses.add(
305+ responses.POST, 'http://localhost:8000/readonlydata',
306+ json={'readonly': True})
307
308 servers = [{'SERVER_ID': 'localhost', 'SCHEME': 'http',
309 'HOST': 'localhost', 'VIRTUAL_HOST': '', 'PORT': '8000'}]
310@@ -260,8 +235,10 @@
311 self.assertEqual(r.context['appserver'], 'localhost')
312 self.assertEqual(r.context['action'], 'set')
313
314+ @responses.activate
315 def test_readonly_confirm_post(self):
316- self.patch_urlopen()
317+ responses.add(
318+ responses.POST, 'http://localhost:8000/readonlydata', json={})
319 self.login_with_staff()
320
321 new_setting = [
322@@ -271,15 +248,17 @@
323 ]
324 with self.settings(APP_SERVERS=new_setting):
325 r = self.client.post('/readonly/localhost/set')
326- data = map(lambda x: x.data, self.reqs)
327+ data = [call.request.body for call in responses.calls]
328 self.assertEqual(data, [
329 "action=set&secret=%s" % settings.READONLY_SECRET
330 ])
331
332 self.assertRedirects(r, '/readonly')
333
334+ @responses.activate
335 def test_update_server_all_appservers(self):
336- self.patch_urlopen()
337+ responses.add(
338+ responses.POST, 'http://localhost:8000/readonlydata', json={})
339 new_setting = [
340 {'SERVER_ID': 'localhost', 'SCHEME': 'http',
341 'HOST': 'localhost', 'VIRTUAL_HOST': '', 'PORT': '8000'},
342@@ -288,14 +267,16 @@
343 ]
344 with self.settings(APP_SERVERS=new_setting):
345 update_server('set')
346- data = map(lambda x: x.data, self.reqs)
347+ data = [call.request.body for call in responses.calls]
348 self.assertEqual(data, [
349 "action=set&secret=%s" % settings.READONLY_SECRET,
350 "action=set&secret=%s" % settings.READONLY_SECRET
351 ])
352
353+ @responses.activate
354 def test_update_server_one_appserver(self):
355- self.patch_urlopen()
356+ responses.add(
357+ responses.POST, 'http://localhost:8000/readonlydata', json={})
358 new_setting = [
359 {'SERVER_ID': 'localhost', 'SCHEME': 'http',
360 'HOST': 'localhost', 'VIRTUAL_HOST': '', 'PORT': '8000'},
361@@ -304,20 +285,22 @@
362 ]
363 with self.settings(APP_SERVERS=new_setting):
364 update_server('set', 'localhost')
365- data = map(lambda x: x.data, self.reqs)
366+ data = [call.request.body for call in responses.calls]
367 self.assertEqual(data, [
368 "action=set&secret=%s" % settings.READONLY_SECRET
369 ])
370
371+ @responses.activate
372 def test_update_server_one_connection(self):
373- self.patch_urlopen()
374+ responses.add(
375+ responses.POST, 'http://localhost:8000/readonlydata', json={})
376 new_setting = [
377 {'SERVER_ID': 'localhost', 'SCHEME': 'http',
378 'HOST': 'localhost', 'VIRTUAL_HOST': '', 'PORT': '8000'},
379 ]
380 with self.settings(APP_SERVERS=new_setting):
381 update_server('set', 'localhost')
382- data = map(lambda x: x.data, self.reqs)
383+ data = [call.request.body for call in responses.calls]
384 self.assertEqual(data, [
385 "action=set&secret=%s" % settings.READONLY_SECRET,
386 ])
387
388=== modified file 'src/identityprovider/tests/test_wsgi.py'
389--- src/identityprovider/tests/test_wsgi.py 2015-11-23 15:20:40 +0000
390+++ src/identityprovider/tests/test_wsgi.py 2018-01-29 17:03:41 +0000
391@@ -7,12 +7,11 @@
392 from mock import Mock
393
394 from identityprovider.tests.utils import SSOBaseTestCase
395-from u1testutils.wsgi_intercept import WSGIInterceptedTestCase
396
397 from identityprovider.wsgi import make_app, BzrRevnoMiddleware
398
399
400-class WsgiTestCase(SSOBaseTestCase, WSGIInterceptedTestCase):
401+class WsgiTestCase(SSOBaseTestCase):
402
403 def setUp(self):
404 super(WsgiTestCase, self).setUp()
405
406=== modified file 'src/identityprovider/tests/utils.py'
407--- src/identityprovider/tests/utils.py 2017-04-19 15:36:34 +0000
408+++ src/identityprovider/tests/utils.py 2018-01-29 17:03:41 +0000
409@@ -256,6 +256,13 @@
410 # loop detection.
411 self.client.cookies['C'] = '1'
412
413+ def unset_env(self, name):
414+ """Temporarily unset an environment variable."""
415+ orig_value = os.environ.get(name)
416+ if orig_value is not None:
417+ self.addCleanup(os.environ.__setitem__, name, orig_value)
418+ del os.environ[name]
419+
420
421 class SSOBaseTestCase(SSOBaseTestCaseMixin, TestCase):
422