Merge lp:~alecu/ubuntu-sso-client/proxy-integration-tests into lp:ubuntu-sso-client

Proposed by Alejandro J. Cura on 2012-01-04
Status: Merged
Approved by: Alejandro J. Cura on 2012-01-11
Approved revision: 838
Merged at revision: 831
Proposed branch: lp:~alecu/ubuntu-sso-client/proxy-integration-tests
Merge into: lp:ubuntu-sso-client
Diff against target: 726 lines (+419/-30)
8 files modified
ubuntu_sso/utils/webclient/common.py (+14/-3)
ubuntu_sso/utils/webclient/gsettings.py (+63/-0)
ubuntu_sso/utils/webclient/libsoup.py (+32/-4)
ubuntu_sso/utils/webclient/qtnetwork.py (+32/-4)
ubuntu_sso/utils/webclient/tests/test_gsettings.py (+131/-0)
ubuntu_sso/utils/webclient/tests/test_webclient.py (+88/-16)
ubuntu_sso/utils/webclient/tests/webclient_demo.py (+51/-0)
ubuntu_sso/utils/webclient/txweb.py (+8/-3)
To merge this branch: bzr merge lp:~alecu/ubuntu-sso-client/proxy-integration-tests
Reviewer Review Type Date Requested Status
Manuel de la Peña (community) 2012-01-04 Approve on 2012-01-11
Natalia Bidart 2012-01-04 Approve on 2012-01-11
Review via email: mp+87538@code.launchpad.net

Commit Message

Proxy aware webclient with QtNetwork and libsoup backends, and integration tests. (LP: #884963 LP: #884968 LP: #884970 LP: #884971 LP: #911844)

Description of the Change

Proxy aware webclient with QtNetwork and libsoup backends, and integration tests. (LP: #884963 LP: #884968 LP: #884970 LP: #884971)
This branch also forces the webclient to take unicode IRIs (instead of ascii URIs) (LP: #911844)

NOTE: this branch needs the squid support in this branch:
 * lp:~mandel/ubuntuone-dev-tools/proxy-testcase

To post a comment you must log in.
Alejandro J. Cura (alecu) wrote :

Proxy aware webclient with QtNetwork and libsoup backends, and integration tests. (LP: #884963 LP: #884968 LP: #884970 LP: #884971)

Natalia Bidart (nataliabidart) wrote :

I have this PEP8 issue:

./ubuntu_sso/utils/webclient/tests/test_gsettings.py:56:17: E202 whitespace before ')'

review: Needs Fixing
Manuel de la Peña (mandel) wrote :

With squid3 the tests won't pass. The solution to this is to make the MockServer timeout the connection that the proxy has. Please take a look at ubuntuone-dev-tools for an example.

review: Needs Fixing
Alejandro J. Cura (alecu) wrote :

> I have this PEP8 issue:
>
> ./ubuntu_sso/utils/webclient/tests/test_gsettings.py:56:17: E202 whitespace
> before ')'

I can't reproduce that in my Precise installation.
Anyway, I'm rewritting that bit of code so it looks better.

836. By Alejandro J. Cura on 2012-01-09

Forcefully disconnect squid3 that uses HTTP/1.1 and lint fixes

Manuel de la Peña (mandel) wrote :

I dont understand why GSETTINGS_CMDLINE is stored as a single string if you are going to do .split() with it. Isn't it more clear to store it as it will be used?

review: Needs Information
Alejandro J. Cura (alecu) wrote :

> I dont understand why GSETTINGS_CMDLINE is stored as a single string if you
> are going to do .split() with it. Isn't it more clear to store it as it will
> be used?

I think it's more clear to store it as just a string; to me using a list of strings for this adds noise to the code.
Also, if we ever need to change that cmdline it's easier to change it this way.

Natalia Bidart (nataliabidart) wrote :

Module docstring for ubuntu_sso/utils/webclient/gsettings.py says:

"""A webclient backend that uses QtNetwork."""

I would guess this is a copy&paste typo?

review: Needs Information
837. By Alejandro J. Cura on 2012-01-10

fix for a docstring

Natalia Bidart (nataliabidart) wrote :

So, I ran the suite in windows and I'm getting:

ubuntu_sso.utils.webclient.tests.test_webclient
  WebClientTestCase
    test_get_iri ... Traceback (most recent call last):
Failure: twisted.internet.defer.TimeoutError: <ubuntu_sso.utils.webclient.tests.
test_webclient.WebClientTestCase testMethod=test_get_iri> (test_get_iri) still r
unning at 8.0 secs
[ERROR]
    test_get_iri_error ... Traceback (most recent call last):
Failure: twisted.internet.defer.TimeoutError: <ubuntu_sso.utils.webclient.tests.
test_webclient.WebClientTestCase testMethod=test_get_iri_error> (test_get_iri_er
ror) still running at 8.0 secs
[ERROR]
    test_method_head ... Traceback (most recent call last):
Failure: twisted.trial.util.DirtyReactorAggregateError: Reactor was unclean.
DelayedCalls: (set twisted.internet.base.DelayedCall.debug = True to debug)
<DelayedCall 0x425ea08 [0.22000002861s] called=0 cancelled=0 SaveSite._updateLog
DateTime()>
<DelayedCall 0x425eaf8 [5.21399998665s] called=0 cancelled=0 onTimeout(<Deferred
 at 0x425ea30>)>
[ERROR]
Traceback (most recent call last):
Failure: twisted.trial.util.DirtyReactorAggregateError: Reactor was unclean.
Selectables:
<<class 'twisted.internet.tcp.Port'> of ubuntu_sso.utils.webclient.tests.test_we
bclient.SaveSite on 50543>
[ERROR]

review: Needs Fixing
Alejandro J. Cura (alecu) wrote :

I tried increasing the test timeout on that TestCase, from 8 to 28, and now all tests pass.
I don't think it's a clean solution, so tomorrow I'll be looking for a better solution.

838. By Alejandro J. Cura on 2012-01-11

127.0.0.1 instead of localhost, because tests on windows timeout before resolving

Natalia Bidart (nataliabidart) wrote :

Looks great now!

review: Approve
Manuel de la Peña (mandel) wrote :

Ran the tests on Ubuntu P and Windows 7, all pass as expected. Code looks great.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'ubuntu_sso/utils/webclient/common.py'
2--- ubuntu_sso/utils/webclient/common.py 2011-12-13 19:26:16 +0000
3+++ ubuntu_sso/utils/webclient/common.py 2012-01-11 04:40:28 +0000
4@@ -17,6 +17,7 @@
5
6 import time
7
8+from httplib2 import iri2uri
9 from oauth import oauth
10 from twisted.internet import defer
11
12@@ -46,7 +47,7 @@
13 self.username = username
14 self.password = password
15
16- def request(self, url, method="GET", extra_headers=None,
17+ def request(self, iri, method="GET", extra_headers=None,
18 oauth_credentials=None):
19 """Return a deferred that will be fired with a Response object."""
20 raise NotImplementedError
21@@ -57,8 +58,18 @@
22 # TODO: get the synchronized timestamp
23 return defer.succeed(time.time())
24
25+ def force_use_proxy(self, settings):
26+ """Setup this webclient to use the given proxy settings."""
27+ raise NotImplementedError
28+
29+ def iri_to_uri(self, iri):
30+ """Transform a unicode iri into a ascii uri."""
31+ if not isinstance(iri, unicode):
32+ raise TypeError
33+ return bytes(iri2uri(iri))
34+
35 @staticmethod
36- def build_oauth_headers(method, url, credentials, timestamp):
37+ def build_oauth_headers(method, uri, credentials, timestamp):
38 """Build an oauth request given some credentials."""
39 consumer = oauth.OAuthConsumer(credentials["consumer_key"],
40 credentials["consumer_secret"])
41@@ -68,7 +79,7 @@
42 if timestamp:
43 parameters["oauth_timestamp"] = timestamp
44 request = oauth.OAuthRequest.from_consumer_and_token(
45- http_url=url,
46+ http_url=uri,
47 http_method=method,
48 parameters=parameters,
49 oauth_consumer=consumer,
50
51=== added file 'ubuntu_sso/utils/webclient/gsettings.py'
52--- ubuntu_sso/utils/webclient/gsettings.py 1970-01-01 00:00:00 +0000
53+++ ubuntu_sso/utils/webclient/gsettings.py 2012-01-11 04:40:28 +0000
54@@ -0,0 +1,63 @@
55+# -*- coding: utf-8 -*-
56+#
57+# Copyright 2011 Canonical Ltd.
58+#
59+# This program is free software: you can redistribute it and/or modify it
60+# under the terms of the GNU General Public License version 3, as published
61+# by the Free Software Foundation.
62+#
63+# This program is distributed in the hope that it will be useful, but
64+# WITHOUT ANY WARRANTY; without even the implied warranties of
65+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
66+# PURPOSE. See the GNU General Public License for more details.
67+#
68+# You should have received a copy of the GNU General Public License along
69+# with this program. If not, see <http://www.gnu.org/licenses/>.
70+"""Retrieve the proxy configuration from Gnome."""
71+
72+import subprocess
73+
74+GSETTINGS_CMDLINE = "gsettings list-recursively org.gnome.system.proxy"
75+
76+
77+def get_proxy_settings():
78+ """Parse the proxy settings as returned by the gsettings executable."""
79+ output = subprocess.check_output(GSETTINGS_CMDLINE.split())
80+ gsettings = {}
81+ base_len = len("org.gnome.system.proxy.")
82+ # pylint: disable=E1103
83+ for line in output.split("\n"):
84+ try:
85+ path, key, value = line.split(" ", 2)
86+ except ValueError:
87+ continue
88+ if value.startswith("'"):
89+ parsed_value = value[1:-1]
90+ elif value.startswith('['):
91+ parsed_value = value
92+ elif value in ('true', 'false'):
93+ parsed_value = (value == 'true')
94+ else:
95+ parsed_value = int(value)
96+ relative_key = (path + "." + key)[base_len:]
97+ gsettings[relative_key] = parsed_value
98+
99+ mode = gsettings["mode"]
100+ if mode == "none":
101+ settings = {}
102+ elif mode == "manual":
103+ settings = {
104+ "host": gsettings["http.host"],
105+ "port": gsettings["http.port"],
106+ }
107+ if gsettings["http.use-authentication"]:
108+ settings.update({
109+ "username": gsettings["http.authentication-user"],
110+ "password": gsettings["http.authentication-password"],
111+ })
112+ else:
113+ # If mode is automatic the PAC javascript should be interpreted
114+ # on each request. That is out of scope so it's ignored for now
115+ settings = {}
116+
117+ return settings
118
119=== modified file 'ubuntu_sso/utils/webclient/libsoup.py'
120--- ubuntu_sso/utils/webclient/libsoup.py 2011-12-20 01:46:51 +0000
121+++ ubuntu_sso/utils/webclient/libsoup.py 2012-01-11 04:40:28 +0000
122@@ -17,6 +17,8 @@
123
124 import httplib
125
126+from collections import defaultdict
127+
128 from twisted.internet import defer
129
130 from ubuntu_sso.utils.webclient.common import (
131@@ -26,6 +28,9 @@
132 WebClientError,
133 )
134
135+URI_ANONYMOUS_TEMPLATE = "http://{host}:{port}/"
136+URI_USERNAME_TEMPLATE = "http://{username}:{password}@{host}:{port}/"
137+
138
139 class WebClient(BaseWebClient):
140 """A webclient with a libsoup backend."""
141@@ -43,7 +48,11 @@
142 def _on_message(self, session, message, d):
143 """Handle the result of an http message."""
144 if message.status_code == httplib.OK:
145- response = Response(message.response_body.data)
146+ headers = defaultdict(list)
147+ response_headers = message.get_property("response-headers")
148+ add_header = lambda key, value, _: headers[key].append(value)
149+ response_headers.foreach(add_header, None)
150+ response = Response(message.response_body.data, headers)
151 d.callback(response)
152 elif message.status_code == httplib.UNAUTHORIZED:
153 e = UnauthorizedError(message.reason_phrase)
154@@ -58,9 +67,10 @@
155 auth.authenticate(self.username, self.password)
156
157 @defer.inlineCallbacks
158- def request(self, url, method="GET", extra_headers=None,
159+ def request(self, iri, method="GET", extra_headers=None,
160 oauth_credentials=None):
161 """Return a deferred that will be fired with a Response object."""
162+ uri = self.iri_to_uri(iri)
163 if extra_headers:
164 headers = dict(extra_headers)
165 else:
166@@ -68,12 +78,12 @@
167
168 if oauth_credentials:
169 timestamp = yield self.get_timestamp()
170- oauth_headers = self.build_oauth_headers(method, url,
171+ oauth_headers = self.build_oauth_headers(method, uri,
172 oauth_credentials, timestamp)
173 headers.update(oauth_headers)
174
175 d = defer.Deferred()
176- message = self.soup.Message.new(method, url)
177+ message = self.soup.Message.new(method, uri)
178
179 for key, value in headers.iteritems():
180 message.request_headers.append(key, value)
181@@ -82,6 +92,24 @@
182 response = yield d
183 defer.returnValue(response)
184
185+ def force_use_proxy(self, settings):
186+ """Setup this webclient to use the given proxy settings."""
187+ # pylint: disable=W0511
188+ proxy_uri = self.get_proxy_uri(settings)
189+ self.session.set_property("proxy-uri", proxy_uri)
190+
191+ def get_proxy_uri(self, settings):
192+ """Get a Soup.URI for the proxy, or None if disabled."""
193+ if "host" in settings and "port" in settings:
194+ template = URI_ANONYMOUS_TEMPLATE
195+ if "username" in settings and "password" in settings:
196+ template = URI_USERNAME_TEMPLATE
197+ uri = template.format(**settings)
198+ return self.soup.URI.new(uri)
199+ else:
200+ # If the proxy host is not set, use no proxy
201+ return None
202+
203 def shutdown(self):
204 """End the soup session for this webclient."""
205 self.session.abort()
206
207=== modified file 'ubuntu_sso/utils/webclient/qtnetwork.py'
208--- ubuntu_sso/utils/webclient/qtnetwork.py 2011-12-13 19:26:16 +0000
209+++ ubuntu_sso/utils/webclient/qtnetwork.py 2012-01-11 04:40:28 +0000
210@@ -15,12 +15,17 @@
211 # with this program. If not, see <http://www.gnu.org/licenses/>.
212 """A webclient backend that uses QtNetwork."""
213
214+import sys
215+
216+from collections import defaultdict
217+
218 from PyQt4.QtCore import (
219 QCoreApplication,
220 QUrl,
221 )
222 from PyQt4.QtNetwork import (
223 QNetworkAccessManager,
224+ QNetworkProxy,
225 QNetworkReply,
226 QNetworkRequest,
227 )
228@@ -32,6 +37,7 @@
229 UnauthorizedError,
230 WebClientError,
231 )
232+from ubuntu_sso.utils.webclient import gsettings
233
234
235 class WebClient(BaseWebClient):
236@@ -44,12 +50,22 @@
237 self.nam.finished.connect(self._handle_finished)
238 self.nam.authenticationRequired.connect(self._handle_authentication)
239 self.replies = {}
240+ self.setup_proxy()
241+
242+ def setup_proxy(self):
243+ """Setup the proxy settings if needed."""
244+ # QtNetwork knows how to use the system settings on both Win and Mac
245+ if sys.platform.startswith("linux"):
246+ settings = gsettings.get_proxy_settings()
247+ if settings:
248+ self.force_use_proxy(settings)
249
250 @defer.inlineCallbacks
251- def request(self, url, method="GET", extra_headers=None,
252+ def request(self, iri, method="GET", extra_headers=None,
253 oauth_credentials=None):
254 """Return a deferred that will be fired with a Response object."""
255- request = QNetworkRequest(QUrl(url))
256+ uri = self.iri_to_uri(iri)
257+ request = QNetworkRequest(QUrl(uri))
258
259 if extra_headers:
260 headers = dict(extra_headers)
261@@ -58,7 +74,7 @@
262
263 if oauth_credentials:
264 timestamp = yield self.get_timestamp()
265- oauth_headers = self.build_oauth_headers(method, url,
266+ oauth_headers = self.build_oauth_headers(method, uri,
267 oauth_credentials, timestamp)
268 headers.update(oauth_headers)
269
270@@ -87,7 +103,10 @@
271 d = self.replies.pop(reply)
272 error = reply.error()
273 if not error:
274- response = Response(reply.readAll())
275+ headers = defaultdict(list)
276+ for key, value in reply.rawHeaderPairs():
277+ headers[str(key)].append(str(value))
278+ response = Response(reply.readAll(), headers)
279 d.callback(response)
280 else:
281 if error == QNetworkReply.AuthenticationRequiredError:
282@@ -96,6 +115,15 @@
283 exception = WebClientError(reply.errorString())
284 d.errback(exception)
285
286+ def force_use_proxy(self, settings):
287+ """Setup this webclient to use the given proxy settings."""
288+ proxy = QNetworkProxy(QNetworkProxy.HttpProxy,
289+ hostName=settings.get("host", ""),
290+ port=settings.get("port", 0),
291+ user=settings.get("username", ""),
292+ password=settings.get("password", ""))
293+ self.nam.setProxy(proxy)
294+
295 def shutdown(self):
296 """Shut down all pending requests (if possible)."""
297 self.nam.deleteLater()
298
299=== added file 'ubuntu_sso/utils/webclient/tests/test_gsettings.py'
300--- ubuntu_sso/utils/webclient/tests/test_gsettings.py 1970-01-01 00:00:00 +0000
301+++ ubuntu_sso/utils/webclient/tests/test_gsettings.py 2012-01-11 04:40:28 +0000
302@@ -0,0 +1,131 @@
303+# -*- coding: utf-8 -*-
304+#
305+# Copyright 2011 Canonical Ltd.
306+#
307+# This program is free software: you can redistribute it and/or modify it
308+# under the terms of the GNU General Public License version 3, as published
309+# by the Free Software Foundation.
310+#
311+# This program is distributed in the hope that it will be useful, but
312+# WITHOUT ANY WARRANTY; without even the implied warranties of
313+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
314+# PURPOSE. See the GNU General Public License for more details.
315+#
316+# You should have received a copy of the GNU General Public License along
317+# with this program. If not, see <http://www.gnu.org/licenses/>.
318+"""Test the gsettings parser."""
319+
320+from twisted.trial.unittest import TestCase
321+
322+from ubuntu_sso.utils.webclient import gsettings
323+
324+# Some settings are not used as described in:
325+# https://bugzilla.gnome.org/show_bug.cgi?id=648237
326+
327+TEMPLATE_GSETTINGS_OUTPUT = """\
328+org.gnome.system.proxy autoconfig-url '{autoconfig_url}'
329+org.gnome.system.proxy ignore-hosts {ignore_hosts:s}
330+org.gnome.system.proxy mode '{mode}'
331+org.gnome.system.proxy.ftp host '{ftp_host}'
332+org.gnome.system.proxy.ftp port {ftp_port}
333+org.gnome.system.proxy.http authentication-password '{auth_password}'
334+org.gnome.system.proxy.http authentication-user '{auth_user}'
335+org.gnome.system.proxy.http host '{http_host}'
336+org.gnome.system.proxy.http port {http_port}
337+org.gnome.system.proxy.http use-authentication {http_use_auth}
338+org.gnome.system.proxy.https host '{https_host}'
339+org.gnome.system.proxy.https port {https_port}
340+org.gnome.system.proxy.socks host '{socks_host}'
341+org.gnome.system.proxy.socks port {socks_port}
342+"""
343+
344+BASE_GSETTINGS_VALUES = {
345+ "autoconfig_url": "",
346+ "ignore_hosts": ["localhost", "127.0.0.0/8"],
347+ "mode": "none",
348+ "ftp_host": "",
349+ "ftp_port": 0,
350+ "auth_password": "",
351+ "auth_user": "",
352+ "http_host": "",
353+ "http_port": 0,
354+ "http_use_auth": "false",
355+ "https_host": "",
356+ "https_port": 0,
357+ "socks_host": "",
358+ "socks_port": 0,
359+}
360+
361+
362+class ProxySettingsTestCase(TestCase):
363+ """Test the getting of the proxy settings."""
364+
365+ def test_gsettings_cmdline_correct(self):
366+ """The command line used to get the proxy settings is the right one."""
367+ expected = "gsettings list-recursively org.gnome.system.proxy".split()
368+ called = []
369+
370+ def append_output(args):
371+ """Append the output and return some settings."""
372+ called.append(args)
373+ return TEMPLATE_GSETTINGS_OUTPUT.format(**BASE_GSETTINGS_VALUES)
374+
375+ self.patch(gsettings.subprocess, "check_output", append_output)
376+ gsettings.get_proxy_settings()
377+ self.assertEqual(called[0], expected)
378+
379+ def test_gsettings_parser_none(self):
380+ """Test a parser of gsettings."""
381+ expected = {}
382+ fake_output = TEMPLATE_GSETTINGS_OUTPUT.format(**BASE_GSETTINGS_VALUES)
383+ self.patch(gsettings.subprocess, "check_output",
384+ lambda _: fake_output)
385+ ps = gsettings.get_proxy_settings()
386+ self.assertEqual(ps, expected)
387+
388+ def test_gsettings_parser_http_anonymous(self):
389+ """Test a parser of gsettings."""
390+ template_values = dict(BASE_GSETTINGS_VALUES)
391+ expected_host = "expected_host"
392+ expected_port = 54321
393+ expected = {
394+ "host": expected_host,
395+ "port": expected_port,
396+ }
397+ template_values.update({
398+ "mode": "manual",
399+ "http_host": expected_host,
400+ "http_port": expected_port,
401+ })
402+ fake_output = TEMPLATE_GSETTINGS_OUTPUT.format(**template_values)
403+ self.patch(gsettings.subprocess, "check_output",
404+ lambda _: fake_output)
405+ ps = gsettings.get_proxy_settings()
406+ self.assertEqual(ps, expected)
407+
408+ def test_gsettings_parser_http_authenticated(self):
409+ """Test a parser of gsettings."""
410+ template_values = dict(BASE_GSETTINGS_VALUES)
411+ expected_host = "expected_host"
412+ expected_port = 54321
413+ expected_user = "carlitos"
414+ expected_password = "very secret password"
415+ expected = {
416+ "host": expected_host,
417+ "port": expected_port,
418+ "username": expected_user,
419+ "password": expected_password,
420+ }
421+ template_values.update({
422+ "mode": "manual",
423+ "http_host": expected_host,
424+ "http_port": expected_port,
425+ "auth_user": expected_user,
426+ "auth_password": expected_password,
427+ "http_use_auth": "true",
428+ })
429+ fake_output = TEMPLATE_GSETTINGS_OUTPUT.format(**template_values)
430+ self.patch(gsettings.subprocess, "check_output",
431+ lambda _: fake_output)
432+ ps = gsettings.get_proxy_settings()
433+ self.assertEqual(ps, expected)
434
435=== modified file 'ubuntu_sso/utils/webclient/tests/test_webclient.py'
436--- ubuntu_sso/utils/webclient/tests/test_webclient.py 2011-12-14 15:29:02 +0000
437+++ ubuntu_sso/utils/webclient/tests/test_webclient.py 2012-01-11 04:40:28 +0000
438@@ -25,6 +25,7 @@
439 from twisted.web import http, resource, server, guard
440
441 from ubuntuone.devtools.testcases import TestCase
442+from ubuntuone.devtools.testcases.squid import SquidTestCase
443
444 from ubuntu_sso.utils import webclient
445
446@@ -50,6 +51,8 @@
447 GUARDED = "guarded"
448 OAUTHRESOURCE = "oauthresource"
449
450+WEBCLIENT_MODULE_NAME = webclient.webclient_module().__name__
451+
452
453 def sample_get_credentials():
454 """Will return the sample credentials right now."""
455@@ -109,6 +112,29 @@
456 return "ERROR: Expected OAuth header not present."
457
458
459+class SaveHTTPChannel(http.HTTPChannel):
460+ """A save protocol to be used in tests."""
461+
462+ protocolInstance = None
463+
464+ def connectionMade(self):
465+ """Keep track of the given protocol."""
466+ SaveHTTPChannel.protocolInstance = self
467+ http.HTTPChannel.connectionMade(self)
468+
469+
470+class SaveSite(server.Site):
471+ """A site that let us know when it closed."""
472+
473+ protocol = SaveHTTPChannel
474+
475+ def __init__(self, *args, **kwargs):
476+ """Create a new instance."""
477+ server.Site.__init__(self, *args, **kwargs)
478+ # we disable the timeout in the tests, we will deal with it manually.
479+ self.timeOut = None
480+
481+
482 class MockWebServer(object):
483 """A mock webserver for testing"""
484
485@@ -134,23 +160,25 @@
486 [cred_factory])
487 root.putChild(GUARDED, guarded_resource)
488
489- site = server.Site(root)
490+ self.site = SaveSite(root)
491 application = service.Application('web')
492 self.service_collection = service.IServiceCollection(application)
493 #pylint: disable=E1101
494- self.tcpserver = internet.TCPServer(0, site)
495+ self.tcpserver = internet.TCPServer(0, self.site)
496 self.tcpserver.setServiceParent(self.service_collection)
497 self.service_collection.startService()
498
499- def get_url(self):
500- """Build the url for this mock server."""
501+ def get_iri(self):
502+ """Build the iri for this mock server."""
503 #pylint: disable=W0212
504 port_num = self.tcpserver._port.getHost().port
505- return "http://localhost:%d/" % port_num
506+ return u"http://127.0.0.1:%d/" % port_num
507
508 def stop(self):
509 """Shut it down."""
510 #pylint: disable=E1101
511+ if self.site.protocol.protocolInstance:
512+ self.site.protocol.protocolInstance.timeoutConnection()
513 return self.service_collection.stopService()
514
515
516@@ -206,40 +234,46 @@
517 yield super(WebClientTestCase, self).setUp()
518 self.ws = MockWebServer()
519 self.addCleanup(self.ws.stop)
520- self.base_url = self.ws.get_url()
521+ self.base_iri = self.ws.get_iri()
522 self.wc = webclient.webclient_factory()
523 self.addCleanup(self.wc.shutdown)
524 # pylint: disable=W0511
525 # TODO: skewed timestamp correction
526
527 @defer.inlineCallbacks
528- def test_get_url(self):
529- """A url is successfully retrieved from the mock webserver."""
530- result = yield self.wc.request(self.base_url + SIMPLERESOURCE)
531+ def test_request_takes_an_iri(self):
532+ """Passing a non-unicode iri fails."""
533+ d = self.wc.request(bytes(self.base_iri + SIMPLERESOURCE))
534+ yield self.assertFailure(d, TypeError)
535+
536+ @defer.inlineCallbacks
537+ def test_get_iri(self):
538+ """Passing in a unicode iri works fine."""
539+ result = yield self.wc.request(self.base_iri + SIMPLERESOURCE)
540 self.assertEqual(SAMPLE_RESOURCE, result.content)
541
542 @defer.inlineCallbacks
543- def test_get_url_error(self):
544+ def test_get_iri_error(self):
545 """The errback is called when there's some error."""
546- yield self.assertFailure(self.wc.request(self.base_url + THROWERROR),
547+ yield self.assertFailure(self.wc.request(self.base_iri + THROWERROR),
548 webclient.WebClientError)
549
550 @defer.inlineCallbacks
551 def test_unauthorized(self):
552 """Detect when a request failed with the UNAUTHORIZED http code."""
553- yield self.assertFailure(self.wc.request(self.base_url + UNAUTHORIZED),
554+ yield self.assertFailure(self.wc.request(self.base_iri + UNAUTHORIZED),
555 webclient.UnauthorizedError)
556
557 @defer.inlineCallbacks
558 def test_method_head(self):
559 """The HTTP method is used."""
560- result = yield self.wc.request(self.base_url + HEADONLY, method="HEAD")
561+ result = yield self.wc.request(self.base_iri + HEADONLY, method="HEAD")
562 self.assertEqual("", result.content)
563
564 @defer.inlineCallbacks
565 def test_send_extra_headers(self):
566 """The extra_headers are sent to the server."""
567- result = yield self.wc.request(self.base_url + VERIFYHEADERS,
568+ result = yield self.wc.request(self.base_iri + VERIFYHEADERS,
569 extra_headers=SAMPLE_HEADERS)
570 self.assertEqual(SAMPLE_RESOURCE, result.content)
571
572@@ -249,17 +283,55 @@
573 other_wc = webclient.webclient_factory(username=SAMPLE_USERNAME,
574 password=SAMPLE_PASSWORD)
575 self.addCleanup(other_wc.shutdown)
576- result = yield other_wc.request(self.base_url + GUARDED)
577+ result = yield other_wc.request(self.base_iri + GUARDED)
578 self.assertEqual(SAMPLE_RESOURCE, result.content)
579
580 @defer.inlineCallbacks
581 def test_request_is_oauth_signed(self):
582 """The request is oauth signed."""
583- result = yield self.wc.request(self.base_url + OAUTHRESOURCE,
584+ result = yield self.wc.request(self.base_iri + OAUTHRESOURCE,
585 oauth_credentials=SAMPLE_CREDENTIALS)
586 self.assertEqual(SAMPLE_RESOURCE, result.content)
587
588
589+class BasicProxyTestCase(SquidTestCase):
590+ """Test that the proxy works at all."""
591+
592+ @defer.inlineCallbacks
593+ def setUp(self):
594+ yield super(BasicProxyTestCase, self).setUp()
595+ self.ws = MockWebServer()
596+ self.addCleanup(self.ws.stop)
597+ self.base_iri = self.ws.get_iri()
598+ self.wc = webclient.webclient_factory()
599+ self.addCleanup(self.wc.shutdown)
600+
601+ def assert_header_contains(self, headers, expected):
602+ """One of the headers matching key must contain a given value."""
603+ self.assertTrue(any(expected in value for value in headers))
604+
605+ @defer.inlineCallbacks
606+ def test_anonymous_proxy_is_used(self):
607+ """The anonymous proxy is used by the webclient."""
608+ settings = self.get_nonauth_proxy_settings()
609+ self.wc.force_use_proxy(settings)
610+ result = yield self.wc.request(self.base_iri + SIMPLERESOURCE)
611+ self.assert_header_contains(result.headers["Via"], "squid")
612+
613+ @defer.inlineCallbacks
614+ def test_authenticated_proxy_is_used(self):
615+ """The authenticated proxy is used by the webclient."""
616+ settings = self.get_auth_proxy_settings()
617+ self.wc.force_use_proxy(settings)
618+ result = yield self.wc.request(self.base_iri + SIMPLERESOURCE)
619+ self.assert_header_contains(result.headers["Via"], "squid")
620+
621+ if WEBCLIENT_MODULE_NAME.endswith(".txweb"):
622+ reason = "txweb does not support proxies."
623+ test_anonymous_proxy_is_used.skip = reason
624+ test_authenticated_proxy_is_used.skip = reason
625+
626+
627 class OAuthTestCase(TestCase):
628 """Test for the oauth signing code."""
629
630
631=== added file 'ubuntu_sso/utils/webclient/tests/webclient_demo.py'
632--- ubuntu_sso/utils/webclient/tests/webclient_demo.py 1970-01-01 00:00:00 +0000
633+++ ubuntu_sso/utils/webclient/tests/webclient_demo.py 2012-01-11 04:40:28 +0000
634@@ -0,0 +1,51 @@
635+# -*- coding: utf-8 -*-
636+#
637+# Copyright 2011 Canonical Ltd.
638+#
639+# This program is free software: you can redistribute it and/or modify it
640+# under the terms of the GNU General Public License version 3, as published
641+# by the Free Software Foundation.
642+#
643+# This program is distributed in the hope that it will be useful, but
644+# WITHOUT ANY WARRANTY; without even the implied warranties of
645+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
646+# PURPOSE. See the GNU General Public License for more details.
647+#
648+# You should have received a copy of the GNU General Public License along
649+# with this program. If not, see <http://www.gnu.org/licenses/>.
650+
651+"""Sample program that uses the Qt webclient."""
652+
653+import sys
654+from PyQt4 import QtGui
655+# need to create the QApplication before installing the reactor
656+QtGui.QApplication(sys.argv)
657+import qt4reactor
658+qt4reactor.install()
659+
660+from pprint import pprint
661+from ubuntu_sso.utils.webclient import qtnetwork
662+from twisted.internet import defer, reactor
663+
664+
665+@defer.inlineCallbacks
666+def do_web_call():
667+ """Wait for the webcall to finish and stop the reactor."""
668+ webclient = qtnetwork.WebClient()
669+ d = webclient.request(u"http://slashdot.org")
670+ try:
671+ response = yield d
672+ pprint(response.headers.items())
673+ finally:
674+ reactor.stop()
675+
676+
677+def main():
678+ """The main function of this test script."""
679+ do_web_call()
680+ # pylint: disable=E1101
681+ reactor.run()
682+
683+
684+if __name__ == "__main__":
685+ main()
686
687=== modified file 'ubuntu_sso/utils/webclient/txweb.py'
688--- ubuntu_sso/utils/webclient/txweb.py 2011-12-13 19:26:16 +0000
689+++ ubuntu_sso/utils/webclient/txweb.py 2012-01-11 04:40:28 +0000
690@@ -32,9 +32,10 @@
691 """A simple web client that does not support proxies, yet."""
692
693 @defer.inlineCallbacks
694- def request(self, url, method="GET", extra_headers=None,
695+ def request(self, iri, method="GET", extra_headers=None,
696 oauth_credentials=None):
697 """Get the page, or fail trying."""
698+ uri = self.iri_to_uri(iri)
699 if extra_headers:
700 headers = dict(extra_headers)
701 else:
702@@ -42,7 +43,7 @@
703
704 if oauth_credentials:
705 timestamp = yield self.get_timestamp()
706- oauth_headers = self.build_oauth_headers(method, url,
707+ oauth_headers = self.build_oauth_headers(method, uri,
708 oauth_credentials, timestamp)
709 headers.update(oauth_headers)
710
711@@ -51,10 +52,14 @@
712 headers["Authorization"] = "Basic " + auth
713
714 try:
715- result = yield client.getPage(url, method=method, headers=headers)
716+ result = yield client.getPage(uri, method=method, headers=headers)
717 response = Response(result)
718 defer.returnValue(response)
719 except error.Error as e:
720 if int(e.status) == http.UNAUTHORIZED:
721 raise UnauthorizedError(e.message)
722 raise WebClientError(e.message)
723+
724+ def force_use_proxy(self, settings):
725+ """Setup this webclient to use the given proxy settings."""
726+ # No proxy support in twisted.web.client

Subscribers

People subscribed via source and target branches