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