Merge lp:~nataliabidart/ubuntu-sso-client/stable-3-0-update into lp:ubuntu-sso-client/stable-3-0

Proposed by Natalia Bidart
Status: Merged
Approved by: Natalia Bidart
Approved revision: 817
Merged at revision: 816
Proposed branch: lp:~nataliabidart/ubuntu-sso-client/stable-3-0-update
Merge into: lp:ubuntu-sso-client/stable-3-0
Diff against target: 1552 lines (+944/-228)
23 files modified
bin/ubuntu-sso-login (+14/-59)
bin/windows-ubuntu-sso-login (+0/-86)
run-tests.bat (+1/-1)
ubuntu_sso/gtk/gui.py (+1/-1)
ubuntu_sso/gtk/tests/test_gui.py (+1/-1)
ubuntu_sso/main/__init__.py (+8/-8)
ubuntu_sso/main/linux.py (+48/-7)
ubuntu_sso/main/tests/test_linux.py (+4/-1)
ubuntu_sso/main/tests/test_windows.py (+78/-51)
ubuntu_sso/main/windows.py (+46/-3)
ubuntu_sso/qt/controllers.py (+7/-3)
ubuntu_sso/qt/tests/test_controllers.py (+18/-1)
ubuntu_sso/utils/__init__.py (+1/-1)
ubuntu_sso/utils/tests/test_tcpactivation.py (+17/-2)
ubuntu_sso/utils/tests/test_txsecrets.py (+1/-1)
ubuntu_sso/utils/ui.py (+2/-2)
ubuntu_sso/utils/webclient/__init__.py (+48/-0)
ubuntu_sso/utils/webclient/common.py (+81/-0)
ubuntu_sso/utils/webclient/libsoup.py (+87/-0)
ubuntu_sso/utils/webclient/qtnetwork.py (+101/-0)
ubuntu_sso/utils/webclient/tests/__init__.py (+17/-0)
ubuntu_sso/utils/webclient/tests/test_webclient.py (+303/-0)
ubuntu_sso/utils/webclient/txweb.py (+60/-0)
To merge this branch: bzr merge lp:~nataliabidart/ubuntu-sso-client/stable-3-0-update
Reviewer Review Type Date Requested Status
Roberto Alsina (community) Approve
Review via email: mp+86283@code.launchpad.net

Commit message

[ Alejandro J. Cura <email address hidden> ]
  - An async webclient with qtnetwork and libsoup backends, to be used for
    proxy support.
  - Use the dedicated time url (LP: #891644).

[ Diego Sarmentero <email address hidden> ]
  - Fixed pep8 issue.
  - Fix double back navigation in SSO reset code page (LP: #862403).

[ Manuel de la Pena <email address hidden> ]
  - Fixed the tests by ensuring that the server and the client are correctly
    closed.
  - Changed the import from ubuntuone-dev-tools so that we do not use the
    deprecated API.

[ Natalia B. Bidart <email address hidden> ]
  - Do not hardcode the app_name when showing the TC_NOT_ACCEPTED message
    (LP: #904917).
  - Pass module for test to u1trial properly.
  - Have a single executable to start the service (LP: #890416).
  - Lint fixes (LP: #890349).

Description of the change

All green in ubuntu, and windows (tested windows 7).

IRL tested on Ubuntu.

To post a comment you must log in.
Revision history for this message
Roberto Alsina (ralsina) wrote :

+1

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'bin/ubuntu-sso-login'
2--- bin/ubuntu-sso-login 2011-09-27 14:06:12 +0000
3+++ bin/ubuntu-sso-login 2011-12-19 19:46:24 +0000
4@@ -1,9 +1,5 @@
5-#!/usr/bin/python
6-
7-# ubuntu-sso-login - Client side log-in utility for Ubuntu One
8-#
9-# Author: Rodney Dawes <rodney.dawes@canonical.com>
10-# Author: Natalia B. Bidart <natalia.bidart@canonical.com>
11+#!/usr/bin/env python
12+# -*- coding: utf-8 -*-
13 #
14 # Copyright 2009-2010 Canonical Ltd.
15 #
16@@ -19,7 +15,7 @@
17 # You should have received a copy of the GNU General Public License along
18 # with this program. If not, see <http://www.gnu.org/licenses/>.
19
20-"""Run the dbus service for UserManagement and ApplicationCredentials."""
21+"""Start the sso service."""
22
23 # Invalid name "ubuntu-sso-login", pylint: disable=C0103
24
25@@ -41,60 +37,19 @@
26 # val = globals()[globalname]
27 # KeyError: 'ROUND_CEiLiNG'
28
29-import signal
30 import sys
31
32-import dbus.mainloop.glib
33-import dbus.service
34-import gtk
35-
36-from dbus.mainloop.glib import DBusGMainLoop
37-
38-from ubuntu_sso import (DBUS_BUS_NAME, DBUS_ACCOUNT_PATH, DBUS_CRED_PATH,
39- DBUS_CREDENTIALS_PATH)
40-from ubuntu_sso.main import SSOLogin, CredentialsManagement
41-
42-from ubuntu_sso.logger import setup_logging
43-
44-# Invalid name "ubuntu-sso-login"
45-# pylint: disable=C0103
46-
47-
48-logger = setup_logging("ubuntu-sso-login")
49-dbus.mainloop.glib.threads_init()
50-gtk.gdk.threads_init()
51-DBusGMainLoop(set_as_default=True)
52-
53-
54-def sighup_handler(*a, **kw):
55- """Stop the service."""
56- # This handler may be called in any thread, so is not thread safe.
57- # See the link below for info:
58- # www.listware.net/201004/gtk-devel-list/115067-unix-signals-in-glib.html
59- #
60- # gtk.main_quit and the logger methods are safe to be called from any
61- # thread. Just don't call other random stuff here.
62- logger.info("Stoping Ubuntu SSO login manager since SIGHUP was received.")
63- gtk.main_quit()
64+if sys.platform == 'win32':
65+ from PyQt4 import QtGui
66+ # need to create the QApplication before installing the reactor
67+ QtGui.QApplication(sys.argv)
68+
69+ # pylint: disable=F0401
70+ import qt4reactor
71+ qt4reactor.install()
72+
73+from ubuntu_sso.main import main
74
75
76 if __name__ == "__main__":
77- # Register DBus service for making sure we run only one instance
78- bus = dbus.SessionBus()
79- name = bus.request_name(DBUS_BUS_NAME,
80- dbus.bus.NAME_FLAG_DO_NOT_QUEUE)
81- if name == dbus.bus.REQUEST_NAME_REPLY_EXISTS:
82- logger.error("Ubuntu SSO login manager already running, quitting.")
83- sys.exit(0)
84-
85- logger.debug("Hooking up SIGHUP with handler %r.", sighup_handler)
86- signal.signal(signal.SIGHUP, sighup_handler)
87-
88- logger.info("Starting Ubuntu SSO login manager for bus %r.", DBUS_BUS_NAME)
89- bus_name = dbus.service.BusName(DBUS_BUS_NAME, bus=dbus.SessionBus())
90- SSOLogin(bus_name, object_path=DBUS_ACCOUNT_PATH)
91- CredentialsManagement(timeout_func=gtk.timeout_add,
92- shutdown_func=gtk.main_quit,
93- bus_name=bus_name, object_path=DBUS_CREDENTIALS_PATH)
94-
95- gtk.main()
96+ main()
97
98=== removed file 'bin/windows-ubuntu-sso-login'
99--- bin/windows-ubuntu-sso-login 2011-11-10 20:14:29 +0000
100+++ bin/windows-ubuntu-sso-login 1970-01-01 00:00:00 +0000
101@@ -1,86 +0,0 @@
102-#!/usr/bin/env python
103-# -*- coding: utf-8 -*-
104-# Authors: Manuel de la Pena <manuel@canonical.com>
105-# Alejandro J. Cura <alecu@canonical.com>
106-#
107-# Copyright 2011 Canonical Ltd.
108-#
109-# This program is free software: you can redistribute it and/or modify it
110-# under the terms of the GNU General Public License version 3, as published
111-# by the Free Software Foundation.
112-#
113-# This program is distributed in the hope that it will be useful, but
114-# WITHOUT ANY WARRANTY; without even the implied warranties of
115-# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
116-# PURPOSE. See the GNU General Public License for more details.
117-#
118-# You should have received a copy of the GNU General Public License along
119-# with this program. If not, see <http://www.gnu.org/licenses/>.
120-"""Start the sso service on a windows machine."""
121-
122-# disable the name warning and complains about twisted
123-# pylint: disable=C0103, E1101, F0401
124-import sys
125-
126-from PyQt4 import QtGui
127-# need to create the QApplication before installing the reactor
128-app = QtGui.QApplication(sys.argv)
129-import qt4reactor
130-qt4reactor.install()
131-
132-from twisted.internet import reactor, defer
133-from twisted.spread.pb import PBServerFactory
134-from twisted.internet.task import LoopingCall
135-from twisted.python import log
136-
137-from ubuntu_sso.logger import setup_logging
138-from ubuntu_sso.main.windows import (
139- CredentialsManagement,
140- LOCALHOST,
141- SSOLogin,
142- UbuntuSSORoot,
143- get_activation_config,
144-)
145-from ubuntu_sso.utils import tcpactivation
146-
147-
148-logger = setup_logging("windows-ubuntu-sso-login")
149-
150-
151-def add_timeout(interval, callback, *args, **kwargs):
152- """Add a timeout callback as a task."""
153- time_out_task = LoopingCall(callback, *args, **kwargs)
154- time_out_task.start(interval/1000, now=False)
155-
156-
157-@defer.inlineCallbacks
158-def main():
159- """Initialize and start this process."""
160- ai = tcpactivation.ActivationInstance(get_activation_config())
161- port = yield ai.get_port()
162-
163- login = SSOLogin('ignored')
164- creds_management = CredentialsManagement(add_timeout, reactor.stop)
165- root = UbuntuSSORoot(sso_login=login, cred_manager=creds_management)
166-
167- reactor.listenTCP(port, PBServerFactory(root), interface=LOCALHOST)
168-
169-
170-def handle_already_started(failure):
171- """Handle the already started error by shutting down this process."""
172- failure.trap(tcpactivation.AlreadyStartedError)
173- print "Ubuntu SSO login manager already running."
174- reactor.stop()
175-
176-
177-def utter_failure(failure):
178- """Handle an utter failure by logging it and quiting."""
179- log.err(failure)
180- reactor.stop()
181-
182-
183-if __name__ == '__main__':
184- d = main()
185- d.addErrback(handle_already_started)
186- d.addErrback(utter_failure)
187- reactor.run()
188
189=== modified file 'run-tests.bat'
190--- run-tests.bat 2011-11-10 19:45:36 +0000
191+++ run-tests.bat 2011-12-19 19:46:24 +0000
192@@ -55,7 +55,7 @@
193 "%PYTHONEXEPATH%\python.exe" setup.py build
194 ECHO Running tests
195 :: execute the tests with a number of ignored linux only modules
196-"%PYTHONEXEPATH%\python.exe" "%PYTHONEXEPATH%\Scripts\u1trial" -c ubuntu_sso -i "test_gui.py, test_linux.py, test_txsecrets.py" --reactor=qt4 --gui %*
197+"%PYTHONEXEPATH%\python.exe" "%PYTHONEXEPATH%\Scripts\u1trial" -c -i "test_gui.py, test_linux.py, test_txsecrets.py" --reactor=qt4 --gui %* ubuntu_sso
198 :: Clean the build from the setupt.py
199 ECHO Cleaning the generated code before running the style checks...
200 "%PYTHONEXEPATH%\python.exe" setup.py clean
201
202=== modified file 'ubuntu_sso/gtk/gui.py'
203--- ubuntu_sso/gtk/gui.py 2011-11-14 12:02:39 +0000
204+++ ubuntu_sso/gtk/gui.py 2011-12-19 19:46:24 +0000
205@@ -743,7 +743,7 @@
206 # check T&C
207 if not self.yes_to_tc_checkbutton.get_active():
208 self._set_warning_message(self.tc_warning_label,
209- TC_NOT_ACCEPTED)
210+ TC_NOT_ACCEPTED % {'app_name': self.app_name})
211 error = True
212
213 captcha_solution = self.captcha_solution_entry.get_text()
214
215=== modified file 'ubuntu_sso/gtk/tests/test_gui.py'
216--- ubuntu_sso/gtk/tests/test_gui.py 2011-11-11 17:12:19 +0000
217+++ ubuntu_sso/gtk/tests/test_gui.py 2011-12-19 19:46:24 +0000
218@@ -1420,7 +1420,7 @@
219 self.ui.join_ok_button.clicked()
220
221 self.assert_correct_label_warning(self.ui.tc_warning_label,
222- gui.TC_NOT_ACCEPTED)
223+ gui.TC_NOT_ACCEPTED % {'app_name': APP_NAME})
224 self.assertNotIn('register_user', self.ui.backend._called)
225
226 def test_warning_is_shown_if_not_captcha_solution(self):
227
228=== modified file 'ubuntu_sso/main/__init__.py'
229--- ubuntu_sso/main/__init__.py 2011-09-27 14:06:12 +0000
230+++ ubuntu_sso/main/__init__.py 2011-12-19 19:46:24 +0000
231@@ -338,16 +338,16 @@
232
233 if sys.platform == 'win32':
234 from ubuntu_sso.main import windows
235- SSOLogin = windows.SSOLogin
236- CredentialsManagement = windows.CredentialsManagement
237+ source = windows
238 TIMEOUT_INTERVAL = 10000000000 # forever
239- thread_execute = windows.blocking
240- get_sso_login_backend = windows.get_sso_login_backend
241 else:
242 from ubuntu_sso.main import linux
243- SSOLogin = linux.SSOLogin
244- CredentialsManagement = linux.CredentialsManagement
245- thread_execute = linux.blocking
246- get_sso_login_backend = linux.get_sso_login_backend
247+ source = linux
248+
249+CredentialsManagement = source.CredentialsManagement
250+get_sso_login_backend = source.get_sso_login_backend
251+main = source.main
252+SSOLogin = source.SSOLogin
253+thread_execute = source.blocking
254
255 # pylint: enable=C0103
256
257=== modified file 'ubuntu_sso/main/linux.py'
258--- ubuntu_sso/main/linux.py 2011-09-29 20:49:45 +0000
259+++ ubuntu_sso/main/linux.py 2011-12-19 19:46:24 +0000
260@@ -1,11 +1,6 @@
261 # -*- coding: utf-8 -*-
262 #
263-# ubuntu_sso.main - main login handling interface
264-#
265-# Author: Natalia Bidart <natalia.bidart@canonical.com>
266-# Author: Alejandro J. Cura <alecu@canonical.com>
267-#
268-# Copyright 2009 Canonical Ltd.
269+# Copyright 2009-2011 Canonical Ltd.
270 #
271 # This program is free software: you can redistribute it and/or modify it
272 # under the terms of the GNU General Public License version 3, as published
273@@ -28,12 +23,19 @@
274 """
275
276 import threading
277+import signal
278+import sys
279
280+import dbus.mainloop.glib
281 import dbus.service
282+import gtk
283+
284
285 from ubuntu_sso import (
286 DBUS_ACCOUNT_PATH,
287+ DBUS_BUS_NAME,
288 DBUS_CREDENTIALS_IFACE,
289+ DBUS_CREDENTIALS_PATH,
290 DBUS_IFACE_USER_NAME,
291 NO_OP,
292 )
293@@ -50,7 +52,7 @@
294 # pylint: disable=C0103
295
296
297-logger = setup_logging("ubuntu_sso.main")
298+logger = setup_logging("ubuntu_sso.main.linux")
299
300
301 def blocking(f, app_name, result_cb, error_cb):
302@@ -410,3 +412,42 @@
303 def get_sso_login_backend():
304 """Get the backend for the Login service."""
305 raise NotImplementedError()
306+
307+
308+def sighup_handler(*a, **kw):
309+ """Stop the service."""
310+ # This handler may be called in any thread, so is not thread safe.
311+ # See the link below for info:
312+ # www.listware.net/201004/gtk-devel-list/115067-unix-signals-in-glib.html
313+ #
314+ # gtk.main_quit and the logger methods are safe to be called from any
315+ # thread. Just don't call other random stuff here.
316+ logger.info("Stoping Ubuntu SSO login manager since SIGHUP was received.")
317+ gtk.main_quit()
318+
319+
320+def main():
321+ """Run the backend service."""
322+ dbus.mainloop.glib.threads_init()
323+ gtk.gdk.threads_init()
324+ dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
325+
326+ bus = dbus.SessionBus()
327+ # Register DBus service for making sure we run only one instance
328+ name = bus.request_name(DBUS_BUS_NAME,
329+ dbus.bus.NAME_FLAG_DO_NOT_QUEUE)
330+ if name == dbus.bus.REQUEST_NAME_REPLY_EXISTS:
331+ logger.error("Ubuntu SSO login manager already running, quitting.")
332+ sys.exit(0)
333+
334+ logger.debug("Hooking up SIGHUP with handler %r.", sighup_handler)
335+ signal.signal(signal.SIGHUP, sighup_handler)
336+
337+ logger.info("Starting Ubuntu SSO login manager for bus %r.", DBUS_BUS_NAME)
338+ bus_name = dbus.service.BusName(DBUS_BUS_NAME, bus=dbus.SessionBus())
339+ SSOLogin(bus_name, object_path=DBUS_ACCOUNT_PATH)
340+ CredentialsManagement(timeout_func=gtk.timeout_add,
341+ shutdown_func=gtk.main_quit,
342+ bus_name=bus_name, object_path=DBUS_CREDENTIALS_PATH)
343+
344+ gtk.main()
345
346=== modified file 'ubuntu_sso/main/tests/test_linux.py'
347--- ubuntu_sso/main/tests/test_linux.py 2011-11-14 18:31:46 +0000
348+++ ubuntu_sso/main/tests/test_linux.py 2011-12-19 19:46:24 +0000
349@@ -585,6 +585,7 @@
350 self.memento = MementoHandler()
351 self.memento.setLevel(logging.DEBUG)
352 ubuntu_sso.main.logger.addHandler(self.memento)
353+ self.addCleanup(ubuntu_sso.main.logger.removeHandler, self.memento)
354
355 @defer.inlineCallbacks
356 def tearDown(self):
357@@ -1204,7 +1205,9 @@
358
359 self.memento = MementoHandler()
360 self.memento.setLevel(logging.DEBUG)
361- ubuntu_sso.main.logger.addHandler(self.memento)
362+ ubuntu_sso.main.linux.logger.addHandler(self.memento)
363+ self.addCleanup(ubuntu_sso.main.linux.logger.removeHandler,
364+ self.memento)
365
366 def assert_dbus_signal_correct(self, signal, signature):
367 """Check that 'signal' is a dbus signal with proper 'signature'."""
368
369=== modified file 'ubuntu_sso/main/tests/test_windows.py' (properties changed: -x to +x)
370--- ubuntu_sso/main/tests/test_windows.py 2011-11-02 16:56:12 +0000
371+++ ubuntu_sso/main/tests/test_windows.py 2011-12-19 19:46:24 +0000
372@@ -28,6 +28,7 @@
373 DeadReferenceError,
374 PBClientFactory,
375 PBServerFactory,
376+ Broker,
377 )
378 from ubuntu_sso import main
379 from ubuntu_sso.main import windows
380@@ -46,7 +47,7 @@
381
382 # because we are using twisted we have java like names C0103 and
383 # the issues that mocker brings with it like W0104
384-# pylint: disable=C0103,W0104
385+# pylint: disable=C0103,W0104,E1101,W0201
386
387
388 class SaveProtocolServerFactory(PBServerFactory):
389@@ -59,6 +60,77 @@
390 self.protocolInstance = protocol
391
392
393+class SaveClientFactory(PBClientFactory):
394+ """Client Factory that knows when we disconnected."""
395+
396+ def __init__(self, connected_d, disconnected_d):
397+ """Create a new instance."""
398+ PBClientFactory.__init__(self)
399+ self.connected_d = connected_d
400+ self.disconnected_d = disconnected_d
401+
402+ def clientConnectionMade(self, broker):
403+ """Connection made."""
404+ PBClientFactory.clientConnectionMade(self, broker)
405+ self.connected_d.callback(True)
406+
407+ def clientConnectionLost(self, connector, reason, reconnecting=0):
408+ """Connection lost."""
409+ self.disconnected_d.callback(True)
410+
411+
412+class ServerProtocol(Broker):
413+ """Server protocol that allows us to clean the tests."""
414+
415+ def connectionLost(self, *a):
416+ self.factory.onConnectionLost.callback(self)
417+
418+
419+class ConnectedTestCase(TestCase):
420+ """Base test case with a client and a server."""
421+
422+ @defer.inlineCallbacks
423+ def setUp(self):
424+ """Set up for the tests."""
425+ yield super(ConnectedTestCase, self).setUp()
426+ self.server_disconnected = defer.Deferred()
427+ self.client_disconnected = defer.Deferred()
428+ self.listener = None
429+ self.connector = None
430+ self.server_factory = None
431+ self.client_factory = None
432+
433+ def setup_client_server(self, sso_root):
434+ """Set tests."""
435+ port = get_sso_pb_port()
436+ self.listener = self._listen_server(sso_root, self.server_disconnected,
437+ port)
438+ connected = defer.Deferred()
439+ self.connector = self._connect_client(connected,
440+ self.client_disconnected, port)
441+ self.addCleanup(self.teardown_client_server)
442+ return connected
443+
444+ def _listen_server(self, sso_root, d, port):
445+ """Start listenting."""
446+ self.server_factory = SaveProtocolServerFactory(sso_root)
447+ self.server_factory.onConnectionLost = d
448+ self.server_factory.protocol = ServerProtocol
449+ return reactor.listenTCP(port, self.server_factory)
450+
451+ def _connect_client(self, d1, d2, port):
452+ """Connect client."""
453+ self.client_factory = SaveClientFactory(d1, d2)
454+ return reactor.connectTCP(LOCALHOST, port, self.client_factory)
455+
456+ def teardown_client_server(self):
457+ """Clean resources."""
458+ self.connector.disconnect()
459+ d = defer.maybeDeferred(self.listener.stopListening)
460+ return defer.gatherResults([d, self.client_disconnected,
461+ self.server_disconnected])
462+
463+
464 class FakeDecoratedObject(object):
465 """An object that has decorators."""
466
467@@ -240,7 +312,7 @@
468 self.assertEqual(name, self.signal_names[signal_index])
469
470
471-class SSOLoginTestCase(SignalHandlingTestCase):
472+class SSOLoginTestCase(ConnectedTestCase, SignalHandlingTestCase):
473 """Test the login class."""
474
475 signal_names = [
476@@ -268,35 +340,12 @@
477 self.login = SSOLogin(None)
478 # start pb
479 self.sso_root = UbuntuSSORoot(sso_login=self.login)
480- self.server_factory = SaveProtocolServerFactory(self.sso_root)
481 # pylint: disable=E1101
482- port = get_sso_pb_port()
483- self.listener = reactor.listenTCP(port, self.server_factory)
484- self.client_factory = PBClientFactory()
485- self.connector = reactor.connectTCP(LOCALHOST, port,
486- self.client_factory)
487+ yield self.setup_client_server(self.sso_root)
488 self.client = yield self._get_client()
489 # pylint: enable=E1101
490
491 @defer.inlineCallbacks
492- def tearDown(self):
493- """Clean reactor."""
494- yield super(SSOLoginTestCase, self).tearDown()
495- if self.server_factory.protocolInstance is not None:
496- self.server_factory.protocolInstance.transport.loseConnection()
497- yield defer.gatherResults([self._tearDownServer(),
498- self._tearDownClient()])
499-
500- def _tearDownServer(self):
501- """Teardown the server."""
502- return defer.maybeDeferred(self.listener.stopListening)
503-
504- def _tearDownClient(self):
505- """Tear down the client."""
506- self.connector.disconnect()
507- return defer.succeed(None)
508-
509- @defer.inlineCallbacks
510 def _get_client(self):
511 """Get the client."""
512 # request the remote object and create a client
513@@ -541,7 +590,7 @@
514 self.mocker.verify()
515
516
517-class CredentialsManagementTestCase(TestCase):
518+class CredentialsManagementTestCase(ConnectedTestCase, TestCase):
519 """Test the management class."""
520
521 @defer.inlineCallbacks
522@@ -556,35 +605,12 @@
523 self.creds.root = self.root
524 # start pb
525 self.sso_root = UbuntuSSORoot(cred_manager=self.creds)
526- self.server_factory = SaveProtocolServerFactory(self.sso_root)
527 # pylint: disable=E1101
528- port = get_sso_pb_port()
529- self.listener = reactor.listenTCP(port, self.server_factory)
530- self.client_factory = PBClientFactory()
531- self.connector = reactor.connectTCP(LOCALHOST, port,
532- self.client_factory)
533+ yield self.setup_client_server(self.sso_root)
534 self.client = yield self._get_client()
535 # pylint: enable=E1101
536
537 @defer.inlineCallbacks
538- def tearDown(self):
539- """Clean reactor."""
540- yield super(CredentialsManagementTestCase, self).tearDown()
541- if self.server_factory.protocolInstance is not None:
542- self.server_factory.protocolInstance.transport.loseConnection()
543- yield defer.gatherResults([self._tearDownServer(),
544- self._tearDownClient()])
545-
546- def _tearDownServer(self):
547- """Teardown the server."""
548- return defer.maybeDeferred(self.listener.stopListening)
549-
550- def _tearDownClient(self):
551- """Tear down the client."""
552- self.connector.disconnect()
553- return defer.succeed(None)
554-
555- @defer.inlineCallbacks
556 def _get_client(self):
557 """Get the client."""
558 # request the remote object and create a client
559@@ -592,6 +618,7 @@
560 remote = yield root.callRemote('get_cred_manager')
561 client = CredentialsManagementClient(remote)
562 yield client.register_to_signals()
563+ self.addCleanup(client.unregister_to_signals)
564 # set the cb
565 for signal_name in ['on_authorization_denied_cb',
566 'on_credentials_found_cb',
567
568=== modified file 'ubuntu_sso/main/windows.py'
569--- ubuntu_sso/main/windows.py 2011-09-27 14:06:12 +0000
570+++ ubuntu_sso/main/windows.py 2011-12-19 19:46:24 +0000
571@@ -1,6 +1,4 @@
572 # -*- coding: utf-8 -*-
573-# Authors: Manuel de la Pena <manuel@canonical.com>
574-# Alejandro J. Cura <alecu@canonical.com>
575 #
576 # Copyright 2011 Canonical Ltd.
577 #
578@@ -26,10 +24,12 @@
579 # pylint: enable=F0401
580
581 from twisted.internet import defer, reactor
582+from twisted.internet.task import LoopingCall
583 from twisted.internet.threads import deferToThread
584 from twisted.spread.pb import (
585 DeadReferenceError,
586 PBClientFactory,
587+ PBServerFactory,
588 Referenceable,
589 Root,
590 )
591@@ -41,7 +41,12 @@
592 SSOLoginRoot,
593 except_to_errdict,
594 )
595-from ubuntu_sso.utils.tcpactivation import ActivationConfig, ActivationClient
596+from ubuntu_sso.utils.tcpactivation import (
597+ ActivationClient,
598+ ActivationConfig,
599+ ActivationInstance,
600+ AlreadyStartedError,
601+)
602
603
604 logger = setup_logging("ubuntu_sso.main.windows")
605@@ -857,3 +862,41 @@
606 root = UbuntuSSOClient()
607 yield root.connect()
608 defer.returnValue(root.sso_login)
609+
610+
611+def add_timeout(interval, callback, *args, **kwargs):
612+ """Add a timeout callback as a task."""
613+ time_out_task = LoopingCall(callback, *args, **kwargs)
614+ time_out_task.start(interval / 1000, now=False)
615+
616+
617+# the reactor does have run and stop methods
618+# pylint: disable=E1101
619+
620+@defer.inlineCallbacks
621+def start_service():
622+ """Initialize and start this process."""
623+ try:
624+ ai = ActivationInstance(get_activation_config())
625+ port = yield ai.get_port()
626+ logger.info("Starting Ubuntu SSO login manager for port %r.", port)
627+
628+ login = SSOLogin('ignored')
629+ creds_management = CredentialsManagement(add_timeout, reactor.stop)
630+ root = UbuntuSSORoot(sso_login=login, cred_manager=creds_management)
631+
632+ reactor.listenTCP(port, PBServerFactory(root), interface=LOCALHOST)
633+ except AlreadyStartedError:
634+ logger.error("Ubuntu SSO login manager already running, quitting.")
635+ reactor.stop()
636+ except:
637+ logger.exception('Can not start Ubuntu SSO login manager:')
638+ reactor.stop()
639+
640+
641+def main():
642+ """Run the backend service."""
643+ start_service()
644+ reactor.run()
645+
646+# pylint: enable=E1101
647
648=== modified file 'ubuntu_sso/qt/controllers.py'
649--- ubuntu_sso/qt/controllers.py 2011-11-11 20:34:57 +0000
650+++ ubuntu_sso/qt/controllers.py 2011-12-19 19:46:24 +0000
651@@ -828,8 +828,12 @@
652 email = unicode(self.view.wizard().forgotten.ui.email_line_edit.text())
653 self.view.wizard().current_user.ui.email_edit.setText(email)
654 self.view.wizard().overlay.hide()
655- self.view.wizard().back()
656- self.view.wizard().back()
657+ current_user_id = self.view.wizard().current_user_page_id
658+ visited_pages = self.view.wizard().visitedPages()
659+ for index in reversed(visited_pages):
660+ if index == current_user_id:
661+ break
662+ self.view.wizard().back()
663
664 def on_password_change_error(self, app_name, error):
665 """Let the user know that there was an error."""
666@@ -844,7 +848,7 @@
667 email = unicode(self.view.wizard().forgotten.ui.email_line_edit.text())
668 code = unicode(self.view.ui.reset_code_line_edit.text())
669 password = unicode(self.view.ui.password_line_edit.text())
670- logger.info('Settig new password for %s and email %s with code %s',
671+ logger.info('Setting new password for %r and email %r with code %r',
672 app_name, email, code)
673 self.backend.set_new_password(app_name, email, code, password)
674
675
676=== modified file 'ubuntu_sso/qt/tests/test_controllers.py'
677--- ubuntu_sso/qt/tests/test_controllers.py 2011-11-11 20:34:57 +0000
678+++ ubuntu_sso/qt/tests/test_controllers.py 2011-12-19 19:46:24 +0000
679@@ -669,6 +669,7 @@
680 self.count_back = 0
681 self.hide_value = False
682 self.text_value = 'mail@mail.com'
683+ self.current_user_page_id = 4
684
685 def wizard(self):
686 """Fake wizard function for view."""
687@@ -690,6 +691,10 @@
688 def setText(self, text):
689 """Fake setText for QLineEdit."""
690 self.text_value = text
691+
692+ def visitedPages(self):
693+ """Return an int list of fake visited pages."""
694+ return [1, 4, 6, 8]
695 # pylint: enable=C0103
696
697
698@@ -2094,5 +2099,17 @@
699 """Test that on_password_changed execute the proper operation."""
700 self.controller.on_password_changed('app_name', '')
701 self.assertTrue(self.controller.view.hide_value)
702- self.assertEqual(self.controller.view.count_back, 2)
703+ times_visited = 2
704+ self.assertEqual(self.controller.view.count_back, times_visited)
705+ self.assertEqual(self.controller.view.text_value, 'mail@mail.com')
706+
707+ def test_on_password_changed_not_visited(self):
708+ """Test that on_password_changed execute the proper operation."""
709+ current_user_page_id = 20
710+ self.patch(self.controller.view, "current_user_page_id",
711+ current_user_page_id)
712+ self.controller.on_password_changed('app_name', '')
713+ self.assertTrue(self.controller.view.hide_value)
714+ times_visited = 4
715+ self.assertEqual(self.controller.view.count_back, times_visited)
716 self.assertEqual(self.controller.view.text_value, 'mail@mail.com')
717
718=== modified file 'ubuntu_sso/utils/__init__.py'
719--- ubuntu_sso/utils/__init__.py 2011-10-06 19:38:19 +0000
720+++ ubuntu_sso/utils/__init__.py 2011-12-19 19:46:24 +0000
721@@ -45,7 +45,7 @@
722
723 CHECKING_INTERVAL = 60 * 60 # in seconds
724 ERROR_INTERVAL = 30 # in seconds
725- SERVER_URL = "http://one.ubuntu.com/"
726+ SERVER_URL = "http://one.ubuntu.com/api/time"
727
728 def __init__(self):
729 """Initialize this instance."""
730
731=== modified file 'ubuntu_sso/utils/tests/test_tcpactivation.py'
732--- ubuntu_sso/utils/tests/test_tcpactivation.py 2011-10-28 10:41:18 +0000
733+++ ubuntu_sso/utils/tests/test_tcpactivation.py 2011-12-19 19:46:24 +0000
734@@ -51,12 +51,25 @@
735 """Echo the data received."""
736 self.transport.write(data)
737
738+ #pylint:disable=E1101
739+ def connectionLost(self, *a):
740+ self.factory.onConnectionLost.callback(self)
741+ #pylint:enable=E1101
742+
743
744 class FakeServerFactory(protocol.Factory):
745 """A factory for the test server."""
746
747 protocol = FakeServerProtocol
748
749+ #pylint:disable=W0201
750+ def __init__(self, testcase=None):
751+ """Create a new instance for the testcase."""
752+ if testcase is not None:
753+ self.onConnectionLost = defer.Deferred()
754+ testcase.addCleanup(lambda: self.onConnectionLost)
755+ #pylint:enable=W0201
756+
757
758 class FakeTransport(object):
759 """A fake transport."""
760@@ -197,7 +210,7 @@
761 @defer.inlineCallbacks
762 def test_is_already_running(self):
763 """The is_already_running method returns True if already started."""
764- f = FakeServerFactory()
765+ f = FakeServerFactory(self)
766 # pylint: disable=E1101
767 listener = reactor.listenTCP(SAMPLE_PORT, f,
768 interface=tcpactivation.LOCALHOST)
769@@ -319,18 +332,20 @@
770 port = yield ai.get_port()
771 self.assertEqual(port, SAMPLE_PORT)
772
773+ #pylint:disable=W0201
774 @defer.inlineCallbacks
775 def test_get_port_fails_if_service_already_started(self):
776 """The get_port method fails if service already started."""
777 ai1 = ActivationInstance(self.config)
778 port1 = yield ai1.get_port()
779- f = FakeServerFactory()
780+ f = FakeServerFactory(self)
781 # pylint: disable=E1101
782 listener = reactor.listenTCP(port1, f,
783 interface=tcpactivation.LOCALHOST)
784 self.addCleanup(listener.stopListening)
785 ai2 = ActivationInstance(self.config)
786 yield self.assertFailure(ai2.get_port(), AlreadyStartedError)
787+ #pylint:enable=W0201
788
789
790 def server_test(config):
791
792=== modified file 'ubuntu_sso/utils/tests/test_txsecrets.py'
793--- ubuntu_sso/utils/tests/test_txsecrets.py 2011-10-28 10:41:18 +0000
794+++ ubuntu_sso/utils/tests/test_txsecrets.py 2011-12-19 19:46:24 +0000
795@@ -22,7 +22,7 @@
796 import dbus.service
797
798 from twisted.internet.defer import inlineCallbacks, returnValue
799-from ubuntuone.devtools.testcase import DBusTestCase
800+from ubuntuone.devtools.testcases.dbus import DBusTestCase
801
802 from ubuntu_sso.utils import txsecrets
803
804
805=== modified file 'ubuntu_sso/utils/ui.py'
806--- ubuntu_sso/utils/ui.py 2011-10-20 14:15:46 +0000
807+++ ubuntu_sso/utils/ui.py 2011-12-19 19:46:24 +0000
808@@ -88,8 +88,8 @@
809 SUCCESS = _('You are now logged into %(app_name)s.')
810 SURNAME_ENTRY = _('Surname')
811 TC_BUTTON = _('Show Terms & Conditions')
812-TC_NOT_ACCEPTED = _('Agreeing to the Ubuntu One Terms & Conditions is ' \
813- 'required to subscribe.')
814+TC_NOT_ACCEPTED = _('Agreeing to the %(app_name)s Terms & Conditions is ' \
815+ 'required to subscribe.')
816 TOS_LABEL = _("You can also find these terms at <a href='%(url)s'>%(url)s</a>")
817 TRY_AGAIN_BUTTON = _('Try again')
818 UNKNOWN_ERROR = _('There was an error when trying to complete the ' \
819
820=== added directory 'ubuntu_sso/utils/webclient'
821=== added file 'ubuntu_sso/utils/webclient/__init__.py'
822--- ubuntu_sso/utils/webclient/__init__.py 1970-01-01 00:00:00 +0000
823+++ ubuntu_sso/utils/webclient/__init__.py 2011-12-19 19:46:24 +0000
824@@ -0,0 +1,48 @@
825+# -*- coding: utf-8 -*-
826+#
827+# Copyright 2011 Canonical Ltd.
828+#
829+# This program is free software: you can redistribute it and/or modify it
830+# under the terms of the GNU General Public License version 3, as published
831+# by the Free Software Foundation.
832+#
833+# This program is distributed in the hope that it will be useful, but
834+# WITHOUT ANY WARRANTY; without even the implied warranties of
835+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
836+# PURPOSE. See the GNU General Public License for more details.
837+#
838+# You should have received a copy of the GNU General Public License along
839+# with this program. If not, see <http://www.gnu.org/licenses/>.
840+"""A common webclient that can use a QtNetwork or libsoup backend."""
841+
842+import sys
843+
844+# pylint: disable=W0611
845+from ubuntu_sso.utils.webclient.common import (
846+ UnauthorizedError,
847+ WebClientError,
848+)
849+
850+
851+def is_qt4reactor_installed():
852+ """Check if the qt4reactor is installed."""
853+ reactor = sys.modules.get("twisted.internet.reactor")
854+ return reactor and getattr(reactor, "qApp", None)
855+
856+
857+def webclient_module():
858+ """Choose the module of the web client."""
859+ if is_qt4reactor_installed():
860+ from ubuntu_sso.utils.webclient import qtnetwork as web_module
861+ else:
862+ # the libsoup backend depends on the twisted + GI bug
863+ # meanwhile, use the txweb sample client
864+ #from ubuntu_sso.utils.webclient import libsoup as web_module
865+ from ubuntu_sso.utils.webclient import txweb as web_module
866+ return web_module
867+
868+
869+def webclient_factory(*args, **kwargs):
870+ """Choose the type of the web client dynamically."""
871+ web_module = webclient_module()
872+ return web_module.WebClient(*args, **kwargs)
873
874=== added file 'ubuntu_sso/utils/webclient/common.py'
875--- ubuntu_sso/utils/webclient/common.py 1970-01-01 00:00:00 +0000
876+++ ubuntu_sso/utils/webclient/common.py 2011-12-19 19:46:24 +0000
877@@ -0,0 +1,81 @@
878+# -*- coding: utf-8 -*-
879+#
880+# Copyright 2011 Canonical Ltd.
881+#
882+# This program is free software: you can redistribute it and/or modify it
883+# under the terms of the GNU General Public License version 3, as published
884+# by the Free Software Foundation.
885+#
886+# This program is distributed in the hope that it will be useful, but
887+# WITHOUT ANY WARRANTY; without even the implied warranties of
888+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
889+# PURPOSE. See the GNU General Public License for more details.
890+#
891+# You should have received a copy of the GNU General Public License along
892+# with this program. If not, see <http://www.gnu.org/licenses/>.
893+"""The common bits of a webclient."""
894+
895+import time
896+
897+from oauth import oauth
898+from twisted.internet import defer
899+
900+
901+class WebClientError(Exception):
902+ """An http error happened while calling the webservice."""
903+
904+
905+class UnauthorizedError(WebClientError):
906+ """The request ended with bad_request, unauthorized or forbidden."""
907+
908+
909+class Response(object):
910+ """A reponse object."""
911+
912+ def __init__(self, content, headers=None):
913+ """Initialize this instance."""
914+ self.content = content
915+ self.headers = headers
916+
917+
918+class BaseWebClient(object):
919+ """The webclient base class, to be extended by backends."""
920+
921+ def __init__(self, username=None, password=None):
922+ """Initialize this instance."""
923+ self.username = username
924+ self.password = password
925+
926+ def request(self, url, method="GET", extra_headers=None,
927+ oauth_credentials=None):
928+ """Return a deferred that will be fired with a Response object."""
929+ raise NotImplementedError
930+
931+ def get_timestamp(self):
932+ """Get a timestamp synchronized with the server."""
933+ # pylint: disable=W0511
934+ # TODO: get the synchronized timestamp
935+ return defer.succeed(time.time())
936+
937+ @staticmethod
938+ def build_oauth_headers(method, url, credentials, timestamp):
939+ """Build an oauth request given some credentials."""
940+ consumer = oauth.OAuthConsumer(credentials["consumer_key"],
941+ credentials["consumer_secret"])
942+ token = oauth.OAuthToken(credentials["token"],
943+ credentials["token_secret"])
944+ parameters = {}
945+ if timestamp:
946+ parameters["oauth_timestamp"] = timestamp
947+ request = oauth.OAuthRequest.from_consumer_and_token(
948+ http_url=url,
949+ http_method=method,
950+ parameters=parameters,
951+ oauth_consumer=consumer,
952+ token=token)
953+ sig_method = oauth.OAuthSignatureMethod_HMAC_SHA1()
954+ request.sign_request(sig_method, consumer, token)
955+ return request.to_header()
956+
957+ def shutdown(self):
958+ """Shut down all pending requests (if possible)."""
959
960=== added file 'ubuntu_sso/utils/webclient/libsoup.py'
961--- ubuntu_sso/utils/webclient/libsoup.py 1970-01-01 00:00:00 +0000
962+++ ubuntu_sso/utils/webclient/libsoup.py 2011-12-19 19:46:24 +0000
963@@ -0,0 +1,87 @@
964+# -*- coding: utf-8 -*-
965+#
966+# Copyright 2011 Canonical Ltd.
967+#
968+# This program is free software: you can redistribute it and/or modify it
969+# under the terms of the GNU General Public License version 3, as published
970+# by the Free Software Foundation.
971+#
972+# This program is distributed in the hope that it will be useful, but
973+# WITHOUT ANY WARRANTY; without even the implied warranties of
974+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
975+# PURPOSE. See the GNU General Public License for more details.
976+#
977+# You should have received a copy of the GNU General Public License along
978+# with this program. If not, see <http://www.gnu.org/licenses/>.
979+"""A webclient backend that uses libsoup."""
980+
981+import httplib
982+
983+from twisted.internet import defer
984+
985+from ubuntu_sso.utils.webclient.common import (
986+ BaseWebClient,
987+ Response,
988+ UnauthorizedError,
989+ WebClientError,
990+)
991+
992+
993+class WebClient(BaseWebClient):
994+ """A webclient with a libsoup backend."""
995+
996+ def __init__(self, *args, **kwargs):
997+ """Initialize this instance."""
998+ super(WebClient, self).__init__(*args, **kwargs)
999+ # pylint: disable=E0611
1000+ from gi.repository import Soup, SoupGNOME
1001+ self.soup = Soup
1002+ self.session = Soup.SessionAsync()
1003+ self.session.add_feature_by_type(SoupGNOME.ProxyResolverGNOME)
1004+ self.session.connect("authenticate", self._on_authenticate)
1005+
1006+ def _on_message(self, session, message, d):
1007+ """Handle the result of an http message."""
1008+ if message.status_code == httplib.OK:
1009+ response = Response(message.response_body.data)
1010+ d.callback(response)
1011+ elif message.status_code == httplib.UNAUTHORIZED:
1012+ e = UnauthorizedError(message.reason_phrase)
1013+ d.errback(e)
1014+ else:
1015+ e = WebClientError(message.reason_phrase)
1016+ d.errback(e)
1017+
1018+ def _on_authenticate(self, sesion, message, auth, retrying, data=None):
1019+ """Handle the "authenticate" signal."""
1020+ if not retrying and self.username and self.password:
1021+ auth.authenticate(self.username, self.password)
1022+
1023+ @defer.inlineCallbacks
1024+ def request(self, url, method="GET", extra_headers=None,
1025+ oauth_credentials=None):
1026+ """Return a deferred that will be fired with a Response object."""
1027+ if extra_headers:
1028+ headers = dict(extra_headers)
1029+ else:
1030+ headers = {}
1031+
1032+ if oauth_credentials:
1033+ timestamp = yield self.get_timestamp()
1034+ oauth_headers = self.build_oauth_headers(method, url,
1035+ oauth_credentials, timestamp)
1036+ headers.update(oauth_headers)
1037+
1038+ d = defer.Deferred()
1039+ message = self.soup.Message.new(method, url)
1040+
1041+ for key, value in headers.iteritems():
1042+ message.request_headers.append(key, value)
1043+
1044+ self.session.queue_message(message, self._on_message, d)
1045+ response = yield d
1046+ defer.returnValue(response)
1047+
1048+ def shutdown(self):
1049+ """End the soup session for this webclient."""
1050+ self.session.abort()
1051
1052=== added file 'ubuntu_sso/utils/webclient/qtnetwork.py'
1053--- ubuntu_sso/utils/webclient/qtnetwork.py 1970-01-01 00:00:00 +0000
1054+++ ubuntu_sso/utils/webclient/qtnetwork.py 2011-12-19 19:46:24 +0000
1055@@ -0,0 +1,101 @@
1056+# -*- coding: utf-8 -*-
1057+#
1058+# Copyright 2011 Canonical Ltd.
1059+#
1060+# This program is free software: you can redistribute it and/or modify it
1061+# under the terms of the GNU General Public License version 3, as published
1062+# by the Free Software Foundation.
1063+#
1064+# This program is distributed in the hope that it will be useful, but
1065+# WITHOUT ANY WARRANTY; without even the implied warranties of
1066+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
1067+# PURPOSE. See the GNU General Public License for more details.
1068+#
1069+# You should have received a copy of the GNU General Public License along
1070+# with this program. If not, see <http://www.gnu.org/licenses/>.
1071+"""A webclient backend that uses QtNetwork."""
1072+
1073+from PyQt4.QtCore import (
1074+ QCoreApplication,
1075+ QUrl,
1076+)
1077+from PyQt4.QtNetwork import (
1078+ QNetworkAccessManager,
1079+ QNetworkReply,
1080+ QNetworkRequest,
1081+)
1082+from twisted.internet import defer
1083+
1084+from ubuntu_sso.utils.webclient.common import (
1085+ BaseWebClient,
1086+ Response,
1087+ UnauthorizedError,
1088+ WebClientError,
1089+)
1090+
1091+
1092+class WebClient(BaseWebClient):
1093+ """A webclient with a qtnetwork backend."""
1094+
1095+ def __init__(self, *args, **kwargs):
1096+ """Initialize this instance."""
1097+ super(WebClient, self).__init__(*args, **kwargs)
1098+ self.nam = QNetworkAccessManager(QCoreApplication.instance())
1099+ self.nam.finished.connect(self._handle_finished)
1100+ self.nam.authenticationRequired.connect(self._handle_authentication)
1101+ self.replies = {}
1102+
1103+ @defer.inlineCallbacks
1104+ def request(self, url, method="GET", extra_headers=None,
1105+ oauth_credentials=None):
1106+ """Return a deferred that will be fired with a Response object."""
1107+ request = QNetworkRequest(QUrl(url))
1108+
1109+ if extra_headers:
1110+ headers = dict(extra_headers)
1111+ else:
1112+ headers = {}
1113+
1114+ if oauth_credentials:
1115+ timestamp = yield self.get_timestamp()
1116+ oauth_headers = self.build_oauth_headers(method, url,
1117+ oauth_credentials, timestamp)
1118+ headers.update(oauth_headers)
1119+
1120+ for key, value in headers.iteritems():
1121+ request.setRawHeader(key, value)
1122+
1123+ d = defer.Deferred()
1124+ if method == "GET":
1125+ reply = self.nam.get(request)
1126+ elif method == "HEAD":
1127+ reply = self.nam.head(request)
1128+ else:
1129+ reply = self.nam.sendCustomRequest(request, method)
1130+ self.replies[reply] = d
1131+ result = yield d
1132+ defer.returnValue(result)
1133+
1134+ def _handle_authentication(self, reply, authenticator):
1135+ """The reply needs authentication."""
1136+ authenticator.setUser(self.username)
1137+ authenticator.setPassword(self.password)
1138+
1139+ def _handle_finished(self, reply):
1140+ """The reply has finished processing."""
1141+ assert reply in self.replies
1142+ d = self.replies.pop(reply)
1143+ error = reply.error()
1144+ if not error:
1145+ response = Response(reply.readAll())
1146+ d.callback(response)
1147+ else:
1148+ if error == QNetworkReply.AuthenticationRequiredError:
1149+ exception = UnauthorizedError(reply.errorString())
1150+ else:
1151+ exception = WebClientError(reply.errorString())
1152+ d.errback(exception)
1153+
1154+ def shutdown(self):
1155+ """Shut down all pending requests (if possible)."""
1156+ self.nam.deleteLater()
1157
1158=== added directory 'ubuntu_sso/utils/webclient/tests'
1159=== added file 'ubuntu_sso/utils/webclient/tests/__init__.py'
1160--- ubuntu_sso/utils/webclient/tests/__init__.py 1970-01-01 00:00:00 +0000
1161+++ ubuntu_sso/utils/webclient/tests/__init__.py 2011-12-19 19:46:24 +0000
1162@@ -0,0 +1,17 @@
1163+# -*- coding: utf-8 -*-
1164+#
1165+# Copyright 2011 Canonical Ltd.
1166+#
1167+# This program is free software: you can redistribute it and/or modify it
1168+# under the terms of the GNU General Public License version 3, as published
1169+# by the Free Software Foundation.
1170+#
1171+# This program is distributed in the hope that it will be useful, but
1172+# WITHOUT ANY WARRANTY; without even the implied warranties of
1173+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
1174+# PURPOSE. See the GNU General Public License for more details.
1175+#
1176+# You should have received a copy of the GNU General Public License along
1177+# with this program. If not, see <http://www.gnu.org/licenses/>.
1178+
1179+"""Tests for the proxy-aware webclient."""
1180
1181=== added file 'ubuntu_sso/utils/webclient/tests/test_webclient.py'
1182--- ubuntu_sso/utils/webclient/tests/test_webclient.py 1970-01-01 00:00:00 +0000
1183+++ ubuntu_sso/utils/webclient/tests/test_webclient.py 2011-12-19 19:46:24 +0000
1184@@ -0,0 +1,303 @@
1185+# -*- coding: utf-8 -*-
1186+#
1187+# Copyright 2011 Canonical Ltd.
1188+#
1189+# This program is free software: you can redistribute it and/or modify it
1190+# under the terms of the GNU General Public License version 3, as published
1191+# by the Free Software Foundation.
1192+#
1193+# This program is distributed in the hope that it will be useful, but
1194+# WITHOUT AN WARRANTY; without even the implied warranties of
1195+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
1196+# PURPOSE. See the GNU General Public License for more details.
1197+#
1198+# You should have received a copy of the GNU General Public License along
1199+# with this program. If not, see <http://www.gnu.org/licenses/>.
1200+"""Integration tests for the proxy-enabled webclient."""
1201+
1202+import os
1203+import sys
1204+import urllib
1205+
1206+from twisted.application import internet, service
1207+from twisted.cred import checkers, portal
1208+from twisted.internet import defer
1209+from twisted.web import http, resource, server, guard
1210+
1211+from ubuntuone.devtools.testcases import TestCase
1212+
1213+from ubuntu_sso.utils import webclient
1214+
1215+ANY_VALUE = object()
1216+SAMPLE_KEY = "result"
1217+SAMPLE_VALUE = "sample result"
1218+SAMPLE_RESOURCE = '{"%s": "%s"}' % (SAMPLE_KEY, SAMPLE_VALUE)
1219+SAMPLE_USERNAME = "peddro"
1220+SAMPLE_PASSWORD = "cantropus"
1221+SAMPLE_CREDENTIALS = dict(
1222+ consumer_key="consumer key",
1223+ consumer_secret="consumer secret",
1224+ token="the real token",
1225+ token_secret="the token secret",
1226+)
1227+SAMPLE_HEADERS = {SAMPLE_KEY: SAMPLE_VALUE}
1228+
1229+SIMPLERESOURCE = "simpleresource"
1230+THROWERROR = "throwerror"
1231+UNAUTHORIZED = "unauthorized"
1232+HEADONLY = "headonly"
1233+VERIFYHEADERS = "verifyheaders"
1234+GUARDED = "guarded"
1235+OAUTHRESOURCE = "oauthresource"
1236+
1237+
1238+def sample_get_credentials():
1239+ """Will return the sample credentials right now."""
1240+ return defer.succeed(SAMPLE_CREDENTIALS)
1241+
1242+
1243+# pylint: disable=C0103
1244+# t.w.resource methods have freeform cased names
1245+
1246+class SimpleResource(resource.Resource):
1247+ """A simple web resource."""
1248+
1249+ def render_GET(self, request):
1250+ """Make a bit of html out of these resource's content."""
1251+ return SAMPLE_RESOURCE
1252+
1253+
1254+class HeadOnlyResource(resource.Resource):
1255+ """A resource that fails if called with a method other than HEAD."""
1256+
1257+ def render_HEAD(self, request):
1258+ """Return a bit of html."""
1259+ return "OK"
1260+
1261+
1262+class VerifyHeadersResource(resource.Resource):
1263+ """A resource that verifies the headers received."""
1264+
1265+ def render_GET(self, request):
1266+ """Make a bit of html out of these resource's content."""
1267+ headers = request.requestHeaders.getRawHeaders(SAMPLE_KEY)
1268+ if headers != [SAMPLE_VALUE]:
1269+ request.setResponseCode(http.BAD_REQUEST)
1270+ return "ERROR: Expected header not present."
1271+ return SAMPLE_RESOURCE
1272+
1273+
1274+class SimpleRealm(object):
1275+ """The same simple resource for all users."""
1276+
1277+ def requestAvatar(self, avatarId, mind, *interfaces):
1278+ """The avatar for this user."""
1279+ if resource.IResource in interfaces:
1280+ return (resource.IResource, SimpleResource(), lambda: None)
1281+ raise NotImplementedError()
1282+
1283+
1284+class OAuthCheckerResource(resource.Resource):
1285+ """A resource that verifies the request was oauth signed."""
1286+
1287+ def render_GET(self, request):
1288+ """Make a bit of html out of these resource's content."""
1289+ header = request.requestHeaders.getRawHeaders("Authorization")[0]
1290+ if header.startswith("OAuth "):
1291+ return SAMPLE_RESOURCE
1292+ request.setResponseCode(http.BAD_REQUEST)
1293+ return "ERROR: Expected OAuth header not present."
1294+
1295+
1296+class MockWebServer(object):
1297+ """A mock webserver for testing"""
1298+
1299+ def __init__(self):
1300+ """Start up this instance."""
1301+ root = resource.Resource()
1302+ root.putChild(SIMPLERESOURCE, SimpleResource())
1303+
1304+ root.putChild(THROWERROR, resource.NoResource())
1305+
1306+ unauthorized_resource = resource.ErrorPage(resource.http.UNAUTHORIZED,
1307+ "Unauthorized", "Unauthorized")
1308+ root.putChild(UNAUTHORIZED, unauthorized_resource)
1309+ root.putChild(HEADONLY, HeadOnlyResource())
1310+ root.putChild(VERIFYHEADERS, VerifyHeadersResource())
1311+ root.putChild(OAUTHRESOURCE, OAuthCheckerResource())
1312+
1313+ db = checkers.InMemoryUsernamePasswordDatabaseDontUse()
1314+ db.addUser(SAMPLE_USERNAME, SAMPLE_PASSWORD)
1315+ test_portal = portal.Portal(SimpleRealm(), [db])
1316+ cred_factory = guard.BasicCredentialFactory("example.org")
1317+ guarded_resource = guard.HTTPAuthSessionWrapper(test_portal,
1318+ [cred_factory])
1319+ root.putChild(GUARDED, guarded_resource)
1320+
1321+ site = server.Site(root)
1322+ application = service.Application('web')
1323+ self.service_collection = service.IServiceCollection(application)
1324+ #pylint: disable=E1101
1325+ self.tcpserver = internet.TCPServer(0, site)
1326+ self.tcpserver.setServiceParent(self.service_collection)
1327+ self.service_collection.startService()
1328+
1329+ def get_url(self):
1330+ """Build the url for this mock server."""
1331+ #pylint: disable=W0212
1332+ port_num = self.tcpserver._port.getHost().port
1333+ return "http://localhost:%d/" % port_num
1334+
1335+ def stop(self):
1336+ """Shut it down."""
1337+ #pylint: disable=E1101
1338+ return self.service_collection.stopService()
1339+
1340+
1341+class FakeReactor(object):
1342+ """A fake reactor object."""
1343+ qApp = "Sample qapp"
1344+
1345+
1346+class ModuleSelectionTestCase(TestCase):
1347+ """Test the functions to choose the qtnet or libsoup backend."""
1348+
1349+ def test_is_qt4reactor_installed_not_installed(self):
1350+ """When the qt4reactor is not installed, it returns false."""
1351+ self.patch(sys, "modules", {})
1352+ self.assertFalse(webclient.is_qt4reactor_installed())
1353+
1354+ def test_is_qt4reactor_installed_installed(self):
1355+ """When the qt4reactor is installed, it returns true."""
1356+ fake_sysmodules = {"twisted.internet.reactor": FakeReactor()}
1357+ self.patch(sys, "modules", fake_sysmodules)
1358+ self.assertTrue(webclient.is_qt4reactor_installed())
1359+
1360+ def assert_module_name(self, module, expected_name):
1361+ """Check the name of a given module."""
1362+ module_filename = os.path.basename(module.__file__)
1363+ module_name = os.path.splitext(module_filename)[0]
1364+ self.assertEqual(module_name, expected_name)
1365+
1366+ def test_webclient_module_qtnetwork(self):
1367+ """Test the module name for the qtnetwork case."""
1368+ self.patch(webclient, "is_qt4reactor_installed", lambda: True)
1369+ module = webclient.webclient_module()
1370+ self.assert_module_name(module, "qtnetwork")
1371+
1372+ def test_webclient_module_libsoup(self):
1373+ """Test the module name for the libsoup case."""
1374+ self.patch(webclient, "is_qt4reactor_installed", lambda: False)
1375+ module = webclient.webclient_module()
1376+ # pylint: disable=W0511
1377+ # TODO: the libsoup backend depends on the twisted + GI bug
1378+ # meanwhile, use the test txweb client
1379+ #self.assert_module_name(module, "libsoup")
1380+ self.assert_module_name(module, "txweb")
1381+
1382+
1383+class WebClientTestCase(TestCase):
1384+ """Test for the webclient."""
1385+
1386+ timeout = 8
1387+
1388+ @defer.inlineCallbacks
1389+ def setUp(self):
1390+ yield super(WebClientTestCase, self).setUp()
1391+ self.ws = MockWebServer()
1392+ self.addCleanup(self.ws.stop)
1393+ self.base_url = self.ws.get_url()
1394+ self.wc = webclient.webclient_factory()
1395+ self.addCleanup(self.wc.shutdown)
1396+ # pylint: disable=W0511
1397+ # TODO: skewed timestamp correction
1398+
1399+ @defer.inlineCallbacks
1400+ def test_get_url(self):
1401+ """A url is successfully retrieved from the mock webserver."""
1402+ result = yield self.wc.request(self.base_url + SIMPLERESOURCE)
1403+ self.assertEqual(SAMPLE_RESOURCE, result.content)
1404+
1405+ @defer.inlineCallbacks
1406+ def test_get_url_error(self):
1407+ """The errback is called when there's some error."""
1408+ yield self.assertFailure(self.wc.request(self.base_url + THROWERROR),
1409+ webclient.WebClientError)
1410+
1411+ @defer.inlineCallbacks
1412+ def test_unauthorized(self):
1413+ """Detect when a request failed with the UNAUTHORIZED http code."""
1414+ yield self.assertFailure(self.wc.request(self.base_url + UNAUTHORIZED),
1415+ webclient.UnauthorizedError)
1416+
1417+ @defer.inlineCallbacks
1418+ def test_method_head(self):
1419+ """The HTTP method is used."""
1420+ result = yield self.wc.request(self.base_url + HEADONLY, method="HEAD")
1421+ self.assertEqual("", result.content)
1422+
1423+ @defer.inlineCallbacks
1424+ def test_send_extra_headers(self):
1425+ """The extra_headers are sent to the server."""
1426+ result = yield self.wc.request(self.base_url + VERIFYHEADERS,
1427+ extra_headers=SAMPLE_HEADERS)
1428+ self.assertEqual(SAMPLE_RESOURCE, result.content)
1429+
1430+ @defer.inlineCallbacks
1431+ def test_send_basic_auth(self):
1432+ """The basic authentication headers are sent."""
1433+ other_wc = webclient.webclient_factory(username=SAMPLE_USERNAME,
1434+ password=SAMPLE_PASSWORD)
1435+ self.addCleanup(other_wc.shutdown)
1436+ result = yield other_wc.request(self.base_url + GUARDED)
1437+ self.assertEqual(SAMPLE_RESOURCE, result.content)
1438+
1439+ @defer.inlineCallbacks
1440+ def test_request_is_oauth_signed(self):
1441+ """The request is oauth signed."""
1442+ result = yield self.wc.request(self.base_url + OAUTHRESOURCE,
1443+ oauth_credentials=SAMPLE_CREDENTIALS)
1444+ self.assertEqual(SAMPLE_RESOURCE, result.content)
1445+
1446+
1447+class OAuthTestCase(TestCase):
1448+ """Test for the oauth signing code."""
1449+
1450+ def parse_oauth_header(self, header):
1451+ """Parse an oauth header into a tuple of (method, params_dict)."""
1452+ params = {}
1453+
1454+ method, params_string = header.split(" ", 1)
1455+ for p in params_string.split(","):
1456+ k, v = p.strip().split("=")
1457+ params[k] = urllib.unquote(v[1:-1])
1458+
1459+ return method, params
1460+
1461+ def test_build_oauth_headers(self):
1462+ """Build the oauth headers for a sample request."""
1463+
1464+ sample_method = "GET"
1465+ sample_url = "http://one.ubuntu.com/"
1466+ timestamp = 1
1467+ expected_params = {
1468+ "oauth_timestamp": str(timestamp),
1469+ "oauth_consumer_key": SAMPLE_CREDENTIALS["consumer_key"],
1470+ "oauth_signature_method": "HMAC-SHA1",
1471+ "oauth_token": SAMPLE_CREDENTIALS["token"],
1472+ "oauth_nonce": ANY_VALUE,
1473+ "oauth_signature": ANY_VALUE,
1474+ }
1475+
1476+ module = webclient.webclient_module()
1477+ headers = module.BaseWebClient.build_oauth_headers(sample_method,
1478+ sample_url, SAMPLE_CREDENTIALS, timestamp)
1479+
1480+ method, params = self.parse_oauth_header(headers["Authorization"])
1481+
1482+ self.assertEqual(method, "OAuth")
1483+
1484+ for k, expected_value in expected_params.iteritems():
1485+ self.assertIn(k, params)
1486+ if expected_value is not ANY_VALUE:
1487+ self.assertEqual(params[k], expected_value)
1488
1489=== added file 'ubuntu_sso/utils/webclient/txweb.py'
1490--- ubuntu_sso/utils/webclient/txweb.py 1970-01-01 00:00:00 +0000
1491+++ ubuntu_sso/utils/webclient/txweb.py 2011-12-19 19:46:24 +0000
1492@@ -0,0 +1,60 @@
1493+# -*- coding: utf-8 -*-
1494+#
1495+# Copyright 2011 Canonical Ltd.
1496+#
1497+# This program is free software: you can redistribute it and/or modify it
1498+# under the terms of the GNU General Public License version 3, as published
1499+# by the Free Software Foundation.
1500+#
1501+# This program is distributed in the hope that it will be useful, but
1502+# WITHOUT ANY WARRANTY; without even the implied warranties of
1503+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
1504+# PURPOSE. See the GNU General Public License for more details.
1505+#
1506+# You should have received a copy of the GNU General Public License along
1507+# with this program. If not, see <http://www.gnu.org/licenses/>.
1508+"""A webclient backend that uses twisted.web.client."""
1509+
1510+import base64
1511+
1512+from twisted.web import client, error, http
1513+from twisted.internet import defer
1514+
1515+from ubuntu_sso.utils.webclient.common import (
1516+ BaseWebClient,
1517+ Response,
1518+ UnauthorizedError,
1519+ WebClientError,
1520+)
1521+
1522+
1523+class WebClient(BaseWebClient):
1524+ """A simple web client that does not support proxies, yet."""
1525+
1526+ @defer.inlineCallbacks
1527+ def request(self, url, method="GET", extra_headers=None,
1528+ oauth_credentials=None):
1529+ """Get the page, or fail trying."""
1530+ if extra_headers:
1531+ headers = dict(extra_headers)
1532+ else:
1533+ headers = {}
1534+
1535+ if oauth_credentials:
1536+ timestamp = yield self.get_timestamp()
1537+ oauth_headers = self.build_oauth_headers(method, url,
1538+ oauth_credentials, timestamp)
1539+ headers.update(oauth_headers)
1540+
1541+ if self.username and self.password:
1542+ auth = base64.b64encode(self.username + ":" + self.password)
1543+ headers["Authorization"] = "Basic " + auth
1544+
1545+ try:
1546+ result = yield client.getPage(url, method=method, headers=headers)
1547+ response = Response(result)
1548+ defer.returnValue(response)
1549+ except error.Error as e:
1550+ if int(e.status) == http.UNAUTHORIZED:
1551+ raise UnauthorizedError(e.message)
1552+ raise WebClientError(e.message)

Subscribers

People subscribed via source and target branches