Merge lp:~alecu/ubuntu-sso-client/restful-client into lp:ubuntu-sso-client

Proposed by Alejandro J. Cura
Status: Merged
Approved by: Alejandro J. Cura
Approved revision: 849
Merged at revision: 833
Proposed branch: lp:~alecu/ubuntu-sso-client/restful-client
Merge into: lp:ubuntu-sso-client
Prerequisite: lp:~nataliabidart/ubuntu-sso-client/unify-signal-broadcaster
Diff against target: 269 lines (+215/-4)
5 files modified
ubuntu_sso/utils/webclient/common.py (+2/-1)
ubuntu_sso/utils/webclient/qtnetwork.py (+5/-3)
ubuntu_sso/utils/webclient/restful.py (+52/-0)
ubuntu_sso/utils/webclient/tests/test_restful.py (+148/-0)
ubuntu_sso/utils/webclient/tests/test_webclient.py (+8/-0)
To merge this branch: bzr merge lp:~alecu/ubuntu-sso-client/restful-client
Reviewer Review Type Date Requested Status
Diego Sarmentero (community) Approve
Natalia Bidart (community) Approve
Review via email: mp+88230@code.launchpad.net

Commit message

An async, proxy-enabled replacement for lazr.restfulclient

Description of the change

An async, proxy-enabled replacement for lazr.restfulclient
An upcoming branch will use this module to remove the threads that use lazr

NOTE! This branch also depends on lp:~alecu/ubuntu-sso-client/proxy-integration-tests

To post a comment you must log in.
847. By Alejandro J. Cura

merged with trunk

Revision history for this message
Diego Sarmentero (diegosarmentero) wrote :

== Python Lint Notices ==

ubuntu_sso/utils/webclient/tests/test_webclient.py:
    343: [E0102, BasicProxyTestCase] class already defined line 305
    347: [E1003, BasicProxyTestCase.setUp] Bad first argument 'BasicProxyTestCase' given to super class

review: Needs Fixing
848. By Alejandro J. Cura

remove duplicated testcase

Revision history for this message
Natalia Bidart (nataliabidart) wrote :

Could you please put the value of reply.readAll() in a variable and use that in the 3 lines that uses it?

Shall you also assert on RestfulClient.__init__ that the iri is unicode?

Rest looks good! I will mark as approved but please change the readAll() thingy.

review: Approve
849. By Alejandro J. Cura

refactor requested by nessita

Revision history for this message
Diego Sarmentero (diegosarmentero) wrote :

+1

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 2012-01-04 21:26:27 +0000
3+++ ubuntu_sso/utils/webclient/common.py 2012-01-12 17:51:24 +0000
4@@ -56,7 +56,8 @@
5 """Get a timestamp synchronized with the server."""
6 # pylint: disable=W0511
7 # TODO: get the synchronized timestamp
8- return defer.succeed(time.time())
9+ timestamp = time.time()
10+ return defer.succeed(int(timestamp))
11
12 def force_use_proxy(self, settings):
13 """Setup this webclient to use the given proxy settings."""
14
15=== modified file 'ubuntu_sso/utils/webclient/qtnetwork.py'
16--- ubuntu_sso/utils/webclient/qtnetwork.py 2012-01-04 21:26:27 +0000
17+++ ubuntu_sso/utils/webclient/qtnetwork.py 2012-01-12 17:51:24 +0000
18@@ -102,17 +102,19 @@
19 assert reply in self.replies
20 d = self.replies.pop(reply)
21 error = reply.error()
22+ content = reply.readAll()
23 if not error:
24 headers = defaultdict(list)
25 for key, value in reply.rawHeaderPairs():
26 headers[str(key)].append(str(value))
27- response = Response(reply.readAll(), headers)
28+ response = Response(bytes(content), headers)
29 d.callback(response)
30 else:
31+ error_string = reply.errorString()
32 if error == QNetworkReply.AuthenticationRequiredError:
33- exception = UnauthorizedError(reply.errorString())
34+ exception = UnauthorizedError(error_string, content)
35 else:
36- exception = WebClientError(reply.errorString())
37+ exception = WebClientError(error_string, content)
38 d.errback(exception)
39
40 def force_use_proxy(self, settings):
41
42=== added file 'ubuntu_sso/utils/webclient/restful.py'
43--- ubuntu_sso/utils/webclient/restful.py 1970-01-01 00:00:00 +0000
44+++ ubuntu_sso/utils/webclient/restful.py 2012-01-12 17:51:24 +0000
45@@ -0,0 +1,52 @@
46+# -*- coding: utf-8 -*-
47+#
48+# Copyright 2011 Canonical Ltd.
49+#
50+# This program is free software: you can redistribute it and/or modify it
51+# under the terms of the GNU General Public License version 3, as published
52+# by the Free Software Foundation.
53+#
54+# This program is distributed in the hope that it will be useful, but
55+# WITHOUT ANY WARRANTY; without even the implied warranties of
56+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
57+# PURPOSE. See the GNU General Public License for more details.
58+#
59+# You should have received a copy of the GNU General Public License along
60+# with this program. If not, see <http://www.gnu.org/licenses/>.
61+"""A proxy-enabled restful client."""
62+
63+import json
64+import urllib
65+
66+from twisted.internet import defer
67+
68+from ubuntu_sso.utils import webclient
69+
70+
71+class RestfulClient(object):
72+ """A proxy-enabled restful client."""
73+
74+ def __init__(self, service_iri, username=None, password=None,
75+ oauth_credentials=None):
76+ """Initialize this instance."""
77+ assert service_iri.endswith("/")
78+ self.service_iri = service_iri
79+ self.webclient = webclient.webclient_factory(username=username,
80+ password=password)
81+ self.oauth_credentials = oauth_credentials
82+
83+ @defer.inlineCallbacks
84+ def restcall(self, method, **kwargs):
85+ """Make a restful call."""
86+ assert isinstance(method, unicode)
87+ for key, value in kwargs.items():
88+ if isinstance(value, basestring):
89+ assert isinstance(value, unicode)
90+ kwargs[key] = value.encode("utf-8")
91+ namespace, operation = method.split(".")
92+ kwargs["ws.op"] = operation
93+ encoded_args = urllib.urlencode(kwargs)
94+ iri = self.service_iri + namespace + "?" + encoded_args
95+ creds = self.oauth_credentials
96+ result = yield self.webclient.request(iri, oauth_credentials=creds)
97+ defer.returnValue(json.loads(result.content))
98
99=== added file 'ubuntu_sso/utils/webclient/tests/test_restful.py'
100--- ubuntu_sso/utils/webclient/tests/test_restful.py 1970-01-01 00:00:00 +0000
101+++ ubuntu_sso/utils/webclient/tests/test_restful.py 2012-01-12 17:51:24 +0000
102@@ -0,0 +1,148 @@
103+# -*- coding: utf-8 -*-
104+#
105+# Copyright 2011 Canonical Ltd.
106+#
107+# This program is free software: you can redistribute it and/or modify it
108+# under the terms of the GNU General Public License version 3, as published
109+# by the Free Software Foundation.
110+#
111+# This program is distributed in the hope that it will be useful, but
112+# WITHOUT AN WARRANTY; without even the implied warranties of
113+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
114+# PURPOSE. See the GNU General Public License for more details.
115+#
116+# You should have received a copy of the GNU General Public License along
117+# with this program. If not, see <http://www.gnu.org/licenses/>.
118+"""Tests for the proxy-enabled restful client."""
119+
120+import urlparse
121+
122+from twisted.internet import defer
123+from ubuntuone.devtools.testcases import TestCase
124+
125+from ubuntu_sso.utils.webclient import restful
126+from ubuntu_sso.utils.webclient.common import Response
127+
128+
129+SAMPLE_SERVICE_IRI = u"http://localhost/"
130+SAMPLE_NAMESPACE = u"sample_namespace"
131+SAMPLE_METHOD = u"sample_method"
132+SAMPLE_OPERATION = SAMPLE_NAMESPACE + u"." + SAMPLE_METHOD
133+SAMPLE_ARGS = dict(uno=1, dos=u"2", tres=u"ñandú")
134+SAMPLE_RESPONSE = restful.json.dumps(SAMPLE_ARGS)
135+SAMPLE_USERNAME = "joeuser@example.com"
136+SAMPLE_PASSWORD = "clavesecreta"
137+SAMPLE_OAUTH_CREDS = dict(token="1234", etc="456")
138+
139+
140+class FakeWebClient(object):
141+ """A fake web client."""
142+
143+ def __init__(self, **kwargs):
144+ """Initialize this faker."""
145+ self.return_value = SAMPLE_RESPONSE
146+ self.called = []
147+ self.init_kwargs = kwargs
148+
149+ def request(self, iri, *args, **kwargs):
150+ """Return a deferred that will be fired with a Response object."""
151+ self.called.append((iri, args, kwargs))
152+ return defer.succeed(Response(self.return_value))
153+
154+
155+class BaseTestCase(TestCase):
156+ """The base for the Restful Client testcases."""
157+
158+ @defer.inlineCallbacks
159+ def setUp(self):
160+ """Initialize this test case."""
161+ yield super(BaseTestCase, self).setUp()
162+ self.wc = None
163+ self.patch(restful.webclient, "webclient_factory",
164+ self.webclient_factory)
165+
166+ def webclient_factory(self, **kwargs):
167+ """A factory that saves the webclient created."""
168+ self.wc = FakeWebClient(**kwargs)
169+ return self.wc
170+
171+
172+class RestfulClientTestCase(BaseTestCase):
173+ """Tests for the proxy-enabled Restful Client."""
174+
175+ @defer.inlineCallbacks
176+ def setUp(self):
177+ """Initialize this testcase."""
178+ yield super(RestfulClientTestCase, self).setUp()
179+ self.rc = restful.RestfulClient(SAMPLE_SERVICE_IRI)
180+
181+ def test_has_a_webclient(self):
182+ """The RC has a webclient."""
183+ self.assertEqual(self.rc.webclient, self.wc)
184+
185+ @defer.inlineCallbacks
186+ def test_can_make_calls(self):
187+ """The RC can make webcalls."""
188+ yield self.rc.restcall(SAMPLE_OPERATION, **SAMPLE_ARGS)
189+ self.assertEqual(len(self.wc.called), 1)
190+
191+ @defer.inlineCallbacks
192+ def get_parsed_url(self):
193+ """Call the sample operation, and return the url used."""
194+ yield self.rc.restcall(SAMPLE_OPERATION, **SAMPLE_ARGS)
195+ iri, _, _ = self.wc.called[0]
196+ uri = iri.encode("ascii")
197+ defer.returnValue(urlparse.urlparse(uri))
198+
199+ @defer.inlineCallbacks
200+ def test_restful_namespace_added_to_url(self):
201+ """The restful namespace is added to the url."""
202+ url = yield self.get_parsed_url()
203+ self.assertTrue(url.path.endswith(SAMPLE_NAMESPACE),
204+ "The namespace is included in url")
205+
206+ @defer.inlineCallbacks
207+ def test_restful_method_added_to_url(self):
208+ """The restful method is added to the url."""
209+ url = yield self.get_parsed_url()
210+ url_query = urlparse.parse_qs(url.query)
211+ self.assertEqual(url_query["ws.op"][0], SAMPLE_METHOD)
212+
213+ @defer.inlineCallbacks
214+ def test_arguments_added_to_call(self):
215+ """The keyword arguments are used in the called url."""
216+ url = yield self.get_parsed_url()
217+ url_query = dict(urlparse.parse_qsl(url.query))
218+ del(url_query["ws.op"])
219+ expected = {}
220+ for key, value in SAMPLE_ARGS.items():
221+ if isinstance(value, unicode):
222+ expected[key] = value.encode("utf-8")
223+ else:
224+ expected[key] = str(value)
225+ self.assertEqual(url_query, expected)
226+
227+ @defer.inlineCallbacks
228+ def test_return_value_json_parsed(self):
229+ """The result is json parsed before being returned."""
230+ result = yield self.rc.restcall(SAMPLE_OPERATION)
231+ self.assertEqual(result, SAMPLE_ARGS)
232+
233+
234+class AuthenticationOptionsTestCase(BaseTestCase):
235+ """Tests for the authentication options."""
236+
237+ def test_passes_userpass_to_webclient_init(self):
238+ """The RestfulClient passes the user and pass to the webclient."""
239+ expected = dict(username=SAMPLE_USERNAME, password=SAMPLE_PASSWORD)
240+ restful.RestfulClient(SAMPLE_SERVICE_IRI, **expected)
241+ self.assertEqual(self.wc.init_kwargs, expected)
242+
243+ @defer.inlineCallbacks
244+ def test_passes_oauth_creds_to_request(self):
245+ """The RestfulClient passes the credentials in each request."""
246+ kwargs = dict(oauth_credentials=SAMPLE_OAUTH_CREDS)
247+ rc = restful.RestfulClient(SAMPLE_SERVICE_IRI, **kwargs)
248+ yield rc.restcall(SAMPLE_OPERATION, **SAMPLE_ARGS)
249+ _, _, kwargs = self.wc.called[0]
250+ self.assertEqual(kwargs["oauth_credentials"], SAMPLE_OAUTH_CREDS)
251
252=== modified file 'ubuntu_sso/utils/webclient/tests/test_webclient.py'
253--- ubuntu_sso/utils/webclient/tests/test_webclient.py 2012-01-11 04:37:08 +0000
254+++ ubuntu_sso/utils/webclient/tests/test_webclient.py 2012-01-12 17:51:24 +0000
255@@ -293,6 +293,14 @@
256 oauth_credentials=SAMPLE_CREDENTIALS)
257 self.assertEqual(SAMPLE_RESOURCE, result.content)
258
259+ @defer.inlineCallbacks
260+ def test_returned_content_are_bytes(self):
261+ """The returned content are bytes."""
262+ result = yield self.wc.request(self.base_iri + OAUTHRESOURCE,
263+ oauth_credentials=SAMPLE_CREDENTIALS)
264+ self.assertTrue(isinstance(result.content, bytes),
265+ "The type of %r must be bytes" % result.content)
266+
267
268 class BasicProxyTestCase(SquidTestCase):
269 """Test that the proxy works at all."""

Subscribers

People subscribed via source and target branches