Merge lp:~nataliabidart/ubuntu-sso-client/stable-3-0-update-2.99.3 into lp:ubuntu-sso-client/stable-3-0
- stable-3-0-update-2.99.3
- Merge into stable-3-0
Status: | Merged | ||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Approved by: | Natalia Bidart | ||||||||||||||||
Approved revision: | 825 | ||||||||||||||||
Merged at revision: | 822 | ||||||||||||||||
Proposed branch: | lp:~nataliabidart/ubuntu-sso-client/stable-3-0-update-2.99.3 | ||||||||||||||||
Merge into: | lp:ubuntu-sso-client/stable-3-0 | ||||||||||||||||
Diff against target: |
2511 lines (+1165/-442) 21 files modified
bin/ubuntu-sso-login-gtk (+31/-0) run-tests.bat (+120/-70) setup.py (+11/-7) ubuntu_sso/gtk/gui.py (+66/-76) ubuntu_sso/gtk/main.py (+49/-0) ubuntu_sso/gtk/tests/test_gui.py (+113/-191) ubuntu_sso/gtk/tests/test_main.py (+123/-0) ubuntu_sso/keyring/linux.py (+8/-17) ubuntu_sso/main/linux.py (+2/-0) ubuntu_sso/networkstate/linux.py (+0/-2) ubuntu_sso/networkstate/tests/test_linux.py (+104/-20) ubuntu_sso/utils/tests/test_txsecrets.py (+5/-0) ubuntu_sso/utils/webclient/common.py (+43/-7) ubuntu_sso/utils/webclient/libsoup.py (+6/-4) ubuntu_sso/utils/webclient/qtnetwork.py (+7/-5) ubuntu_sso/utils/webclient/restful.py (+13/-5) ubuntu_sso/utils/webclient/tests/test_restful.py (+38/-27) ubuntu_sso/utils/webclient/tests/test_timestamp.py (+140/-0) ubuntu_sso/utils/webclient/tests/test_webclient.py (+131/-1) ubuntu_sso/utils/webclient/timestamp.py (+74/-0) ubuntu_sso/utils/webclient/txweb.py (+81/-10) |
||||||||||||||||
To merge this branch: | bzr merge lp:~nataliabidart/ubuntu-sso-client/stable-3-0-update-2.99.3 | ||||||||||||||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Alejandro J. Cura (community) | Approve | ||
Review via email: mp+90944@code.launchpad.net |
Commit message
[ Natalia B. Bidart <email address hidden> ]
- Skipping failing tests (see bug #920591).
- Make webclient an installable module.
- Make the GTK UI a separated executable script (LP: #917373).
[ Diego Sarmentero <email address hidden> ]
- Delete signal removal to keep listening to network state changes (LP: #920591).
[ Alejandro J. Cura <email address hidden> ]
- Restfulclient calls are now POST; response.headers are now case-insensitive
dicts; OAuth timestamp sync with the server (LP: #916034).
[ Manuel de la Pena <email address hidden> ]
- Add the possibility to skip the lint checks on windows when passing the
/skip-lint parameter to run-tests.bat (LP: #918248).
Description of the change
Ubuntu One Auto Pilot (otto-pilot) wrote : | # |
The attempt to merge lp:~nataliabidart/ubuntu-sso-client/stable-3-0-update-2.99.3 into lp:ubuntu-sso-client/stable-3-0 failed. Below is the output from the failed tests.
Running test suite for ubuntu_sso
** WARNING **: Trying to register gtype 'GMountMountFlags' as enum when in fact it is of type 'GFlags'
** WARNING **: Trying to register gtype 'GDriveStartFlags' as enum when in fact it is of type 'GFlags'
Xlib: extension "RANDR" missing on display ":99".
ubuntu_
AccountTestCase
test_
test_
test_
test_
test_
test_
test_
test_
test_
test_
test_
test_
test_
test_
test_
test_
test_
test_
test_
test_
test_
test_
EnvironOverri
test_
test_
test_
twisted.
TestCase
runTest ... [OK]
ubuntu_
TimestampedAu
test_
ubuntu_
BasicTestCase
runTest ... [OK]
ClearCredenti
test_
test_
CredentialsAu
test_
- 825. By Natalia Bidart
-
- Fixed lint issues.
Preview Diff
1 | === added file 'bin/ubuntu-sso-login-gtk' |
2 | --- bin/ubuntu-sso-login-gtk 1970-01-01 00:00:00 +0000 |
3 | +++ bin/ubuntu-sso-login-gtk 2012-01-31 21:52:24 +0000 |
4 | @@ -0,0 +1,31 @@ |
5 | +#!/usr/bin/env python |
6 | +# -*- coding: utf-8 -*- |
7 | +# |
8 | +# Copyright 2012 Canonical Ltd. |
9 | +# |
10 | +# This program is free software: you can redistribute it and/or modify it |
11 | +# under the terms of the GNU General Public License version 3, as published |
12 | +# by the Free Software Foundation. |
13 | +# |
14 | +# This program is distributed in the hope that it will be useful, but |
15 | +# WITHOUT ANY WARRANTY; without even the implied warranties of |
16 | +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR |
17 | +# PURPOSE. See the GNU General Public License for more details. |
18 | +# |
19 | +# You should have received a copy of the GNU General Public License along |
20 | +# with this program. If not, see <http://www.gnu.org/licenses/>. |
21 | + |
22 | +"""Start the sso GTK UI.""" |
23 | + |
24 | +# Invalid name "ubuntu-sso-login-gtk", pylint: disable=C0103 |
25 | +# Access to a protected member, pylint: disable=W0212 |
26 | + |
27 | +from ubuntu_sso.gtk.main import parse_args, main |
28 | + |
29 | +from dbus.mainloop.glib import DBusGMainLoop |
30 | +DBusGMainLoop(set_as_default=True) |
31 | + |
32 | + |
33 | +if __name__ == "__main__": |
34 | + args = parse_args() |
35 | + main(**dict(args._get_kwargs())) |
36 | |
37 | === modified file 'run-tests.bat' |
38 | --- run-tests.bat 2012-01-17 15:09:12 +0000 |
39 | +++ run-tests.bat 2012-01-31 21:52:24 +0000 |
40 | @@ -1,70 +1,120 @@ |
41 | -:: Author: Manuel de la Pena <manuel@canonical.com> |
42 | -:: |
43 | -:: Copyright 2010 Canonical Ltd. |
44 | -:: |
45 | -:: This program is free software: you can redistribute it and/or modify it |
46 | -:: under the terms of the GNU General Public License version 3, as published |
47 | -:: by the Free Software Foundation. |
48 | -:: |
49 | -:: This program is distributed in the hope that it will be useful, but |
50 | -:: WITHOUT ANY WARRANTY; without even the implied warranties of |
51 | -:: MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR |
52 | -:: PURPOSE. See the GNU General Public License for more details. |
53 | -:: |
54 | -:: You should have received a copy of the GNU General Public License along |
55 | -:: with this program. If not, see <http://www.gnu.org/licenses/>. |
56 | -@ECHO off |
57 | -:: We could have Python 2.6 or 2.7 on Windows. In order to check availability, |
58 | -:: we should first check for 2.7, and run the tests, otherwise fall back to 2.6. |
59 | -SET PYTHONEXEPATH="" |
60 | -:: This is very annoying; FOR /F will work differently depending on the output |
61 | -:: of reg which is not consistent between OS versions (XP, 7). We must choose |
62 | -:: the tokens according to OS version. |
63 | -SET PYTHONPATHTOKENS=3 |
64 | -VER | FIND "XP" > nul |
65 | -IF %ERRORLEVEL% == 0 SET PYTHONPATHTOKENS=4 |
66 | -ECHO Checking if python 2.7 is in the system |
67 | -:: Look for python 2.7 |
68 | -FOR /F "tokens=%PYTHONPATHTOKENS%" %%A IN ('REG QUERY HKLM\Software\Python\PythonCore\2.7\InstallPath /ve') DO @SET PYTHONEXEPATH=%%A |
69 | -IF NOT %PYTHONEXEPATH% == "" GOTO :PYTHONPRESENT |
70 | -ECHO Checking if python 2.6 is in the system |
71 | -:: we do not have python 2.7 in the system, try to find 2.6 |
72 | -FOR /F "tokens=%PYTHONPATHTOKENS%" %%A IN ('REG QUERY HKLM\Software\Python\PythonCore\2.6\InstallPath /ve') DO @SET PYTHONEXEPATH=%%A |
73 | -IF NOT %PYTHONEXEPATH% == "" GOTO :PYTHONPRESENT |
74 | - |
75 | -:: we do not have python (2.6 or 2.7) this could hapen in the case that the |
76 | -:: user installed the 32version in a 64 machine, let check if the software was installed in the wow key |
77 | - |
78 | -:: Look for python 2.7 in WoW64 |
79 | -ECHO Checking if python 2.7 32 is in the system |
80 | -FOR /F "tokens=%PYTHONPATHTOKENS%" %%A IN ('REG QUERY HKLM\Software\Wow6432Node\Python\PythonCore\2.7\InstallPath /ve') DO @SET PYTHONEXEPATH=%%A |
81 | -IF NOT %PYTHONEXEPATH% == "" GOTO :PYTHONPRESENT |
82 | -ECHO Checking if python 2.6 32 is in the system |
83 | -:: we do not have python 2.7 in the system, try to find 2.6 |
84 | -FOR /F "tokens=%PYTHONPATHTOKENS%" %%A IN ('REG QUERY HKLM\Software\Wow6432Node\Python\PythonCore\2.6\InstallPath /ve') DO @SET PYTHONEXEPATH=%%A |
85 | -IF NOT %PYTHONEXEPATH% == "" GOTO :PYTHONPRESENT |
86 | - |
87 | -ECHO Please ensure you have python installed |
88 | -GOTO :END |
89 | - |
90 | - |
91 | -:PYTHONPRESENT |
92 | -ECHO Python found, building auto-generated modules... |
93 | -:: call setup.py build so that the qt uic is called |
94 | -::START "Build code" /D%CD% /WAIT "%PYTHONEXEPATH%\python.exe" setup.py build |
95 | -"%PYTHONEXEPATH%\python.exe" setup.py build |
96 | -ECHO Running tests |
97 | -:: execute the tests with a number of ignored linux only modules |
98 | -"%PYTHONEXEPATH%\python.exe" "%PYTHONEXEPATH%\Scripts\u1trial" -c -i "test_gui.py, test_linux.py, test_txsecrets.py" --reactor=qt4 --gui %* ubuntu_sso |
99 | -:: Clean the build from the setupt.py |
100 | -ECHO Cleaning the generated code before running the style checks... |
101 | -"%PYTHONEXEPATH%\python.exe" setup.py clean |
102 | -ECHO Performing style checks... |
103 | -SET IGNORE_LINT="ubuntu_sso\gtk,ubuntu_sso\networkstate\linux.py,ubuntu_sso\main\linux.py,ubuntu_sso\main\tests\test_linux.py,ubuntu_sso\utils\txsecrets.py,ubuntu_sso\utils\tests\test_txsecrets.py,ubuntu_sso\tests\bin,bin\ubuntu-sso-login" |
104 | -"%PYTHONEXEPATH%\python.exe" "%PYTHONEXEPATH%\Scripts\u1lint" -i "%IGNORE_LINT%" ubuntu_sso |
105 | -:: test for style if we can, if pep8 is not present, move to the end |
106 | -"%PYTHONEXEPATH%\Scripts\pep8.exe" --repeat . |
107 | -:: Delete the temp folders |
108 | -RMDIR /q /s _trial_temp |
109 | -DEL /q .coverage |
110 | -:END |
111 | +:: Copyright 2010-12 Canonical Ltd. |
112 | +:: |
113 | +:: This program is free software: you can redistribute it and/or modify it |
114 | +:: under the terms of the GNU General Public License version 3, as published |
115 | +:: by the Free Software Foundation. |
116 | +:: |
117 | +:: This program is distributed in the hope that it will be useful, but |
118 | +:: WITHOUT ANY WARRANTY; without even the implied warranties of |
119 | +:: MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR |
120 | +:: PURPOSE. See the GNU General Public License for more details. |
121 | +:: |
122 | +:: You should have received a copy of the GNU General Public License along |
123 | +:: with this program. If not, see <http://www.gnu.org/licenses/>. |
124 | + |
125 | +@ECHO off |
126 | + |
127 | +:: We could have Python 2.6 or 2.7 on Windows. In order to check availability, |
128 | +:: we should first check for 2.7, and run the tests, otherwise fall back to 2.6. |
129 | +SET REGQUERY27="REG QUERY HKLM\Software\Python\PythonCore\2.7\InstallPath /ve" |
130 | +SET REGQUERY2732="REG QUERY HKLM\Software\Wow6432Node\Python\PythonCore\2.7\InstallPath /ve" |
131 | +SET REGQUERY26="REG QUERY HKLM\Software\Python\PythonCore\2.6\InstallPath /ve" |
132 | +SET REGQUERY2632="REG QUERY HKLM\Software\Wow6432Node\Python\PythonCore\2.6\InstallPath /ve" |
133 | +SET PYTHONEXEPATH="" |
134 | + |
135 | +:: This is very annoying; FOR /F will work differently depending on the output |
136 | +:: of reg which is not consistent between OS versions (XP, 7). We must choose |
137 | +:: the tokens according to OS version. |
138 | +:: |
139 | +SET PYTHONPATHTOKENS=3 |
140 | +VER | FIND "XP" > nul |
141 | +IF %ERRORLEVEL% == 0 ( |
142 | + SET PYTHONPATHTOKENS=4) |
143 | + |
144 | +ECHO Checking if python 2.7 is in the system |
145 | + |
146 | +:: Look for python 2.7 |
147 | +FOR /F "tokens=%PYTHONPATHTOKENS%" %%A IN ('%REGQUERY27%') DO @SET PYTHONEXEPATH=%%A |
148 | + |
149 | +IF NOT %PYTHONEXEPATH% == "" ( |
150 | + GOTO :PYTHONPRESENT) |
151 | + |
152 | +ECHO Checking if python 2.6 is in the system |
153 | +:: we do not have python 2.7 in the system, try to find 2.6 |
154 | +FOR /F "tokens=%PYTHONPATHTOKENS%" %%A IN ('%REGQUERY26%') DO @SET PYTHONEXEPATH=%%A |
155 | + |
156 | +IF NOT %PYTHONEXEPATH% == "" ( |
157 | + GOTO :PYTHONPRESENT) |
158 | + |
159 | +:: we do not have python (2.6 or 2.7) this could hapen in the case that the |
160 | +:: user installed the 32version in a 64 machine, let check if the software was installed in the wow key |
161 | + |
162 | +:: Look for python 2.7 in WoW64 |
163 | +ECHO Checking if python 2.7 32 is in the system |
164 | +FOR /F "tokens=%PYTHONPATHTOKENS%" %%A IN ('%REGQUERY2732%') DO @SET PYTHONEXEPATH=%%A |
165 | + |
166 | +IF NOT %PYTHONEXEPATH% == "" ( |
167 | + GOTO :PYTHONPRESENT) |
168 | + |
169 | +ECHO Checking if python 2.6 32 is in the system |
170 | +:: we do not have python 2.7 in the system, try to find 2.6 |
171 | +FOR /F "tokens=%PYTHONPATHTOKENS%" %%A IN ('%REGQUERY2632%') DO @SET PYTHONEXEPATH=%%A |
172 | +IF NOT %PYTHONEXEPATH% == "" ( |
173 | + GOTO :PYTHONPRESENT) |
174 | + |
175 | +ECHO Please ensure you have python installed |
176 | +GOTO :END |
177 | + |
178 | + |
179 | +:PYTHONPRESENT |
180 | + |
181 | +:: throw the first parameter away if is /skip-lint, |
182 | +:: the way we do this is to ensure that /skip-lint |
183 | +:: is the first parameter and copy all the rest in a loop |
184 | +:: the main reason for that is that %* is not affected |
185 | +:: by SHIFT, that is, it allways have all passed parameters |
186 | + |
187 | +SET PARAMS=%* |
188 | +SET SKIPLINT=0 |
189 | +IF "%1" == "/skip-lint" ( |
190 | + SET SKIPLINT=1 |
191 | + GOTO :CLEANPARAMS |
192 | +)ELSE ( |
193 | + GOTO :CONTINUEBATCH) |
194 | + |
195 | +:CLEANPARAMS |
196 | +SHIFT |
197 | +SET PARAMS=%1 |
198 | +:GETREST |
199 | +SHIFT |
200 | +if [%1]==[] ( |
201 | + GOTO CONTINUEBATCH) |
202 | +SET PARAMS=%PARAMS% %1 |
203 | +GOTO GETREST |
204 | +:CONTINUEBATCH |
205 | + |
206 | +ECHO Python found, building auto-generated modules... |
207 | +:: call setup.py build so that the qt uic is called |
208 | +::START "Build code" /D%CD% /WAIT "%PYTHONEXEPATH%\python.exe" setup.py build |
209 | +"%PYTHONEXEPATH%\python.exe" setup.py build |
210 | +ECHO Running tests |
211 | +:: execute the tests with a number of ignored linux only modules |
212 | +"%PYTHONEXEPATH%\python.exe" "%PYTHONEXEPATH%\Scripts\u1trial" -i "test_linux.py, test_txsecrets.py" -p "ubuntu_sso\gtk" --reactor=qt4 --gui %PARAMS% ubuntu_sso |
213 | +:: Clean the build from the setupt.py |
214 | +ECHO Cleaning the generated code |
215 | +"%PYTHONEXEPATH%\python.exe" setup.py clean |
216 | + |
217 | +IF %SKIPLINT% == 1 ( |
218 | + ECHO Skipping style checks |
219 | + GOTO :CLEAN) |
220 | + |
221 | +ECHO Performing style checks... |
222 | +SET IGNORE_LINT="ubuntu_sso\gtk,ubuntu_sso\networkstate\linux.py,ubuntu_sso\main\linux.py,ubuntu_sso\main\tests\test_linux.py,ubuntu_sso\utils\txsecrets.py,ubuntu_sso\utils\tests\test_txsecrets.py,ubuntu_sso\tests\bin,bin\ubuntu-sso-login" |
223 | +"%PYTHONEXEPATH%\python.exe" "%PYTHONEXEPATH%\Scripts\u1lint" -i "%IGNORE_LINT%" ubuntu_sso |
224 | +:: test for style if we can, if pep8 is not present, move to the end |
225 | +"%PYTHONEXEPATH%\Scripts\pep8.exe" --repeat . |
226 | +:CLEAN |
227 | +:: Delete the temp folders |
228 | +RMDIR /q /s _trial_temp |
229 | +DEL /q .coverage |
230 | +:END |
231 | |
232 | === modified file 'setup.py' |
233 | --- setup.py 2012-01-17 18:30:01 +0000 |
234 | +++ setup.py 2012-01-31 21:52:24 +0000 |
235 | @@ -349,7 +349,7 @@ |
236 | }, |
237 | }, |
238 | # add the console script so that py2exe compiles it |
239 | - 'console': ['bin/windows-ubuntu-sso-login'], |
240 | + 'console': ['bin/ubuntu-sso-login'], |
241 | 'zipfile': None, |
242 | } |
243 | return _data_files, _extra |
244 | @@ -357,9 +357,12 @@ |
245 | if sys.platform == 'win32': |
246 | data_files, extra = setup_windows() |
247 | else: |
248 | - data_files = [('share/dbus-1/services', ['data/com.ubuntu.sso.service']), |
249 | - ('lib/ubuntu-sso-client', ['bin/ubuntu-sso-login']), |
250 | - ('share/ubuntu-sso-client/data/gtk', ['data/gtk/ui.glade'])] |
251 | + data_files = [ |
252 | + ('lib/ubuntu-sso-client', ['bin/ubuntu-sso-login']), |
253 | + ('lib/ubuntu-sso-client', ['bin/ubuntu-sso-login-gtk']), |
254 | + ('share/dbus-1/services', ['data/com.ubuntu.sso.service']), |
255 | + ('share/ubuntu-sso-client/data/gtk', ['data/gtk/ui.glade']), |
256 | + ] |
257 | extra = {} |
258 | |
259 | DistUtilsExtra.auto.setup( |
260 | @@ -376,12 +379,13 @@ |
261 | data_files=data_files, |
262 | packages=[ |
263 | 'ubuntu_sso', |
264 | - 'ubuntu_sso.utils', |
265 | + 'ubuntu_sso.gtk', |
266 | 'ubuntu_sso.keyring', |
267 | + 'ubuntu_sso.main', |
268 | 'ubuntu_sso.networkstate', |
269 | - 'ubuntu_sso.main', |
270 | - 'ubuntu_sso.gtk', |
271 | 'ubuntu_sso.qt', |
272 | + 'ubuntu_sso.utils', |
273 | + 'ubuntu_sso.utils.webclient', |
274 | 'ubuntu_sso.xdg_base_directory', |
275 | ], |
276 | cmdclass=cmdclass, |
277 | |
278 | === modified file 'ubuntu_sso/gtk/gui.py' |
279 | --- ubuntu_sso/gtk/gui.py 2012-01-17 15:09:12 +0000 |
280 | +++ ubuntu_sso/gtk/gui.py 2012-01-31 21:52:24 +0000 |
281 | @@ -1,9 +1,5 @@ |
282 | # -*- coding: utf-8 -*- |
283 | # |
284 | -# ubuntu_sso.gui - GUI for login and registration |
285 | -# |
286 | -# Author: Natalia Bidart <natalia.bidart@canonical.com> |
287 | -# |
288 | # Copyright 2010 Canonical Ltd. |
289 | # |
290 | # This program is free software: you can redistribute it and/or modify it |
291 | @@ -22,20 +18,18 @@ |
292 | |
293 | import logging |
294 | import os |
295 | +import sys |
296 | import tempfile |
297 | import webbrowser |
298 | |
299 | from functools import wraps |
300 | |
301 | -import dbus |
302 | import gtk |
303 | |
304 | -from twisted.internet.defer import inlineCallbacks |
305 | +from twisted.internet import defer |
306 | |
307 | from ubuntu_sso import ( |
308 | - DBUS_ACCOUNT_PATH, |
309 | - DBUS_BUS_NAME, |
310 | - DBUS_IFACE_USER_NAME, |
311 | + main, |
312 | NO_OP, |
313 | utils, |
314 | ) |
315 | @@ -101,6 +95,9 @@ |
316 | HELP_TEXT_COLOR = gtk.gdk.Color("#bfbfbf") |
317 | WARNING_TEXT_COLOR = gtk.gdk.Color("red") |
318 | |
319 | +USER_CANCELLATION = 10 |
320 | +LOGIN_SUCCESS = REGISTRATION_SUCCESS = 0 |
321 | + |
322 | |
323 | def log_call(f): |
324 | """Decorator to log call funtions.""" |
325 | @@ -183,9 +180,11 @@ |
326 | class UbuntuSSOClientGUI(object): |
327 | """Ubuntu single sign-on GUI.""" |
328 | |
329 | - def __init__(self, app_name, tc_url='', help_text='', |
330 | - window_id=0, login_only=False): |
331 | + def __init__(self, app_name, **kwargs): |
332 | """Create the GUI and initialize widgets.""" |
333 | + logger.debug('UbuntuSSOClientGUI: app_name %r, kwargs %r.', |
334 | + app_name, kwargs) |
335 | + |
336 | gtk.link_button_set_uri_hook(NO_OP) |
337 | |
338 | self._captcha_filename = tempfile.mktemp() |
339 | @@ -195,11 +194,15 @@ |
340 | |
341 | self.app_name = app_name |
342 | self.app_label = '<b>%s</b>' % self.app_name |
343 | - self.tc_url = tc_url |
344 | - self.help_text = help_text |
345 | - self.login_only = login_only |
346 | - |
347 | - self.close_callback = NO_OP |
348 | + self.ping_url = kwargs.get('ping_url', '') |
349 | + self.tc_url = kwargs.get('tc_url', '') |
350 | + self.help_text = kwargs.get('help_text', '') |
351 | + self.login_only = kwargs.get('login_only', False) |
352 | + window_id = kwargs.get('window_id', 0) |
353 | + self.close_callback = kwargs.get('close_callback', NO_OP) |
354 | + self.backend = None |
355 | + # the following 3 callbacks will be removed as soon as |
356 | + # the backend stop using them |
357 | self.login_success_callback = NO_OP |
358 | self.registration_success_callback = NO_OP |
359 | self.user_cancellation_callback = NO_OP |
360 | @@ -252,23 +255,12 @@ |
361 | |
362 | self.window.set_icon_name('ubuntu-logo') |
363 | |
364 | - self.bus = dbus.SessionBus() |
365 | - obj = self.bus.get_object(bus_name=DBUS_BUS_NAME, |
366 | - object_path=DBUS_ACCOUNT_PATH, |
367 | - follow_name_owner_changes=True) |
368 | - self.iface_name = DBUS_IFACE_USER_NAME |
369 | - self.backend = dbus.Interface(object=obj, |
370 | - dbus_interface=self.iface_name) |
371 | - logger.debug('UbuntuSSOClientGUI: backend created: %r', self.backend) |
372 | - |
373 | self.pages = (self.enter_details_vbox, self.processing_vbox, |
374 | self.verify_email_vbox, self.finish_vbox, |
375 | self.tc_browser_vbox, self.login_vbox, |
376 | self.request_password_token_vbox, |
377 | self.set_new_password_vbox) |
378 | |
379 | - self._append_pages() |
380 | - |
381 | self._signals = { |
382 | 'CaptchaGenerated': |
383 | self._filter_by_app_name(self.on_captcha_generated), |
384 | @@ -297,7 +289,6 @@ |
385 | 'PasswordChangeError': |
386 | self._filter_by_app_name(self.on_password_change_error), |
387 | } |
388 | - self._setup_signals() |
389 | |
390 | if window_id != 0: |
391 | # be as robust as possible: |
392 | @@ -314,7 +305,18 @@ |
393 | logger.exception(msg, window_id) |
394 | |
395 | self.yes_to_updates_checkbutton.hide() |
396 | - |
397 | + self.start_backend() |
398 | + |
399 | + @defer.inlineCallbacks |
400 | + def start_backend(self): |
401 | + """Start the backend, show the window when ready.""" |
402 | + client = yield main.get_sso_client() |
403 | + self.backend = client.sso_login |
404 | + |
405 | + logger.debug('UbuntuSSOClientGUI: backend created: %r', self.backend) |
406 | + |
407 | + self._setup_signals() |
408 | + self._append_pages() |
409 | self.window.show() |
410 | |
411 | @property |
412 | @@ -353,22 +355,14 @@ |
413 | |
414 | def _setup_signals(self): |
415 | """Bind signals to callbacks to be able to test the pages.""" |
416 | - iface = self.iface_name |
417 | for signal, method in self._signals.iteritems(): |
418 | - actual = self._signals_receivers.get((iface, signal)) |
419 | + actual = self._signals_receivers.get(signal) |
420 | if actual is not None: |
421 | - msg = 'Signal %r is already connected with %r at iface %r.' |
422 | - logger.warning(msg, signal, actual, iface) |
423 | - |
424 | - match = self.bus.add_signal_receiver(method, signal_name=signal, |
425 | - dbus_interface=iface) |
426 | - logger.debug('Connecting signal %r with method %r at iface %r.' \ |
427 | - 'Match: %r', signal, method, iface, match) |
428 | - self._signals_receivers[(iface, signal)] = method |
429 | - |
430 | - def _debug(self, *args, **kwargs): |
431 | - """Do some debugging.""" |
432 | - print args, kwargs |
433 | + msg = 'Signal %r is already connected with %r.' |
434 | + logger.warning(msg, signal, actual) |
435 | + |
436 | + match = self.backend.connect_to_signal(signal, method) |
437 | + self._signals_receivers[signal] = match |
438 | |
439 | def _add_spinner_to_container(self, container, legend=None): |
440 | """Add a spinner to 'container'.""" |
441 | @@ -651,10 +645,6 @@ |
442 | |
443 | # GTK callbacks |
444 | |
445 | - def run(self): |
446 | - """Run the application.""" |
447 | - gtk.main() |
448 | - |
449 | def connect(self, signal_name, handler, *args, **kwargs): |
450 | """Connect 'signal_name' with 'handler'.""" |
451 | logger.debug('connect: signal %r, handler %r, args %r, kwargs, %r', |
452 | @@ -680,13 +670,8 @@ |
453 | if os.path.exists(self._captcha_filename): |
454 | os.remove(self._captcha_filename) |
455 | |
456 | - # remove the signals from DBus |
457 | - remove = self.bus.remove_signal_receiver |
458 | - for (iface, signal) in self._signals_receivers.keys(): |
459 | - method = self._signals_receivers.pop((iface, signal)) |
460 | - logger.debug('Removing signal %r with method %r at iface %r.', |
461 | - signal, method, iface) |
462 | - remove(method, signal_name=signal, dbus_interface=iface) |
463 | + for signal, match in self._signals_receivers.iteritems(): |
464 | + self.backend.disconnect_from_signal(signal, match) |
465 | |
466 | # hide the main window |
467 | if self.window is not None: |
468 | @@ -696,14 +681,18 @@ |
469 | while gtk.events_pending(): |
470 | gtk.main_iteration() |
471 | |
472 | + return_code = LOGIN_SUCCESS |
473 | if not self._done: |
474 | self.user_cancellation_callback(self.app_name) |
475 | + return_code = USER_CANCELLATION |
476 | |
477 | # call user defined callback |
478 | logger.info('Calling custom close_callback %r with params %r, %r', |
479 | self.close_callback, args, kwargs) |
480 | self.close_callback(*args, **kwargs) |
481 | |
482 | + sys.exit(return_code) |
483 | + |
484 | def on_sign_in_button_clicked(self, *args, **kwargs): |
485 | """User wants to sign in, present the Login page.""" |
486 | self._set_current_page(self.login_vbox) |
487 | @@ -780,12 +769,18 @@ |
488 | |
489 | email = self.user_email |
490 | password = self.user_password |
491 | - f = self.backend.validate_email |
492 | - logger.info('Calling validate_email with email %r, password <hidden>' \ |
493 | - ', app_name %r and email_token %r.', email, self.app_name, |
494 | + |
495 | + args = (self.app_name, email, password, email_token) |
496 | + if self.ping_url: |
497 | + f = self.backend.validate_email_with_ping |
498 | + args = args + (self.ping_url,) |
499 | + else: |
500 | + f = self.backend.validate_email |
501 | + |
502 | + logger.info('Calling validate_email with email %r, password <hidden>, ' |
503 | + 'app_name %r and email_token %r.', email, self.app_name, |
504 | email_token) |
505 | - f(self.app_name, email, password, email_token, |
506 | - reply_handler=NO_OP, error_handler=NO_OP) |
507 | + f(*args, reply_handler=NO_OP, error_handler=NO_OP) |
508 | |
509 | self._set_current_page(self.processing_vbox) |
510 | |
511 | @@ -812,9 +807,14 @@ |
512 | if error: |
513 | return |
514 | |
515 | - f = self.backend.login |
516 | - f(self.app_name, email, password, |
517 | - reply_handler=NO_OP, error_handler=NO_OP) |
518 | + args = (self.app_name, email, password) |
519 | + if self.ping_url: |
520 | + f = self.backend.login_with_ping |
521 | + args = args + (self.ping_url,) |
522 | + else: |
523 | + f = self.backend.login |
524 | + |
525 | + f(*args, reply_handler=NO_OP, error_handler=NO_OP) |
526 | |
527 | self._set_current_page(self.processing_vbox) |
528 | self.user_email = email |
529 | @@ -1021,15 +1021,10 @@ |
530 | self._set_current_page(self.enter_details_vbox, warning_text=msg) |
531 | |
532 | @log_call |
533 | - @inlineCallbacks |
534 | def on_email_validated(self, app_name, email, *args, **kwargs): |
535 | """User email was successfully verified.""" |
536 | - self._done = True |
537 | - result = yield self.registration_success_callback(self.app_name, email) |
538 | - if result == 0: |
539 | - self.finish_success() |
540 | - else: |
541 | - self.finish_error() |
542 | + self.registration_success_callback(self.app_name, email) |
543 | + self.finish_success() |
544 | |
545 | @log_call |
546 | def on_email_validation_error(self, app_name, error, *args, **kwargs): |
547 | @@ -1042,15 +1037,10 @@ |
548 | self._set_current_page(self.verify_email_vbox, warning_text=msg) |
549 | |
550 | @log_call |
551 | - @inlineCallbacks |
552 | def on_logged_in(self, app_name, email, *args, **kwargs): |
553 | """User was successfully logged in.""" |
554 | - self._done = True |
555 | - result = yield self.login_success_callback(self.app_name, email) |
556 | - if result == 0: |
557 | - self.finish_success() |
558 | - else: |
559 | - self.finish_error() |
560 | + self.login_success_callback(self.app_name, email) |
561 | + self.finish_success() |
562 | |
563 | @log_call |
564 | def on_login_error(self, app_name, error, *args, **kwargs): |
565 | |
566 | === added file 'ubuntu_sso/gtk/main.py' |
567 | --- ubuntu_sso/gtk/main.py 1970-01-01 00:00:00 +0000 |
568 | +++ ubuntu_sso/gtk/main.py 2012-01-31 21:52:24 +0000 |
569 | @@ -0,0 +1,49 @@ |
570 | +# -*- coding: utf-8 -*- |
571 | +# |
572 | +# Copyright 2012 Canonical Ltd. |
573 | +# |
574 | +# This program is free software: you can redistribute it and/or modify it |
575 | +# under the terms of the GNU General Public License version 3, as published |
576 | +# by the Free Software Foundation. |
577 | +# |
578 | +# This program is distributed in the hope that it will be useful, but |
579 | +# WITHOUT ANY WARRANTY; without even the implied warranties of |
580 | +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR |
581 | +# PURPOSE. See the GNU General Public License for more details. |
582 | +# |
583 | +# You should have received a copy of the GNU General Public License along |
584 | +# with this program. If not, see <http://www.gnu.org/licenses/>. |
585 | + |
586 | +"""Main module to open the GTK UI.""" |
587 | + |
588 | +import argparse |
589 | + |
590 | +import gtk |
591 | + |
592 | +from ubuntu_sso.gtk.gui import UbuntuSSOClientGUI |
593 | + |
594 | + |
595 | +def parse_args(): |
596 | + """Parse sys.argv options.""" |
597 | + parser = argparse.ArgumentParser(description='Open the GTK SSO UI.') |
598 | + parser.add_argument('--app_name', required=True, |
599 | + help='the name of the application to retrieve credentials for') |
600 | + parser.add_argument('--ping_url', default='', |
601 | + help='a link to be used as the ping url (to notify about new tokens)') |
602 | + parser.add_argument('--tc_url', default='', |
603 | + help='a link to be used as Terms & Conditions url') |
604 | + parser.add_argument('--help_text', default='', |
605 | + help='extra text that will be shown below the headers') |
606 | + parser.add_argument('--window_id', type=int, default=0, |
607 | + help='the window id to be set transient for the SSO GTK dialogs') |
608 | + parser.add_argument('--login_only', action='store_true', default=False, |
609 | + help='whether the SSO GTK UI should only offer login or not') |
610 | + |
611 | + args = parser.parse_args() |
612 | + return args |
613 | + |
614 | + |
615 | +def main(**kwargs): |
616 | + """Start the GTK mainloop and open the main window.""" |
617 | + UbuntuSSOClientGUI(close_callback=gtk.main_quit, **kwargs) |
618 | + gtk.main() |
619 | |
620 | === modified file 'ubuntu_sso/gtk/tests/test_gui.py' |
621 | --- ubuntu_sso/gtk/tests/test_gui.py 2012-01-17 15:09:12 +0000 |
622 | +++ ubuntu_sso/gtk/tests/test_gui.py 2012-01-31 21:52:24 +0000 |
623 | @@ -1,10 +1,6 @@ |
624 | # -*- coding: utf-8 -*- |
625 | # |
626 | -# test_gui - tests for ubuntu_sso.gui |
627 | -# |
628 | -# Author: Natalia Bidart <natalia.bidart@canonical.com> |
629 | -# |
630 | -# Copyright 2010 Canonical Ltd. |
631 | +# Copyright 2010-2012 Canonical Ltd. |
632 | # |
633 | # This program is free software: you can redistribute it and/or modify it |
634 | # under the terms of the GNU General Public License version 3, as published |
635 | @@ -25,7 +21,6 @@ |
636 | |
637 | from collections import defaultdict |
638 | |
639 | -import dbus |
640 | import gtk |
641 | import webkit |
642 | |
643 | @@ -36,7 +31,9 @@ |
644 | from ubuntu_sso.utils.ui import UNKNOWN_ERROR |
645 | from ubuntu_sso.gtk import gui |
646 | from ubuntu_sso.tests import (APP_NAME, TC_URL, HELP_TEXT, CAPTCHA_ID, |
647 | - CAPTCHA_SOLUTION, EMAIL, EMAIL_TOKEN, NAME, PASSWORD, RESET_PASSWORD_TOKEN) |
648 | + CAPTCHA_SOLUTION, EMAIL, EMAIL_TOKEN, NAME, PASSWORD, PING_URL, |
649 | + RESET_PASSWORD_TOKEN, |
650 | +) |
651 | |
652 | |
653 | # Access to a protected member 'yyy' of a client class |
654 | @@ -47,17 +44,32 @@ |
655 | |
656 | |
657 | class FakedSSOBackend(object): |
658 | - """Fake a SSO Backend (acts as a dbus.Interface as well).""" |
659 | + """Fake a SSO Backend.""" |
660 | |
661 | def __init__(self, *args, **kwargs): |
662 | self._args = args |
663 | self._kwargs = kwargs |
664 | self._called = {} |
665 | - for i in ('generate_captcha', 'login', 'register_user', |
666 | - 'validate_email', 'request_password_reset_token', |
667 | + self.callbacks = defaultdict(list) |
668 | + |
669 | + for i in ('generate_captcha', 'login', 'login_with_ping', |
670 | + 'register_user', 'validate_email', |
671 | + 'validate_email_with_ping', |
672 | + 'request_password_reset_token', |
673 | 'set_new_password'): |
674 | setattr(self, i, self._record_call(i)) |
675 | |
676 | + def connect_to_signal(self, signal_name, callback): |
677 | + """Connect a callback to a given signal.""" |
678 | + self.callbacks[signal_name].append(callback) |
679 | + return callback |
680 | + |
681 | + def disconnect_from_signal(self, signal_name, match): |
682 | + """Disconnect from a given signal.""" |
683 | + self.callbacks[signal_name].remove(match) |
684 | + if len(self.callbacks[signal_name]) == 0: |
685 | + self.callbacks.pop(signal_name) |
686 | + |
687 | def _record_call(self, func_name): |
688 | """Store values when calling 'func_name'.""" |
689 | |
690 | @@ -68,42 +80,11 @@ |
691 | return inner |
692 | |
693 | |
694 | -class FakedDbusObject(object): |
695 | - """Fake a dbus.""" |
696 | - |
697 | - def __init__(self, *args, **kwargs): |
698 | - """Init.""" |
699 | - self._args = args |
700 | - self._kwargs = kwargs |
701 | - |
702 | - |
703 | -class FakedSessionBus(object): |
704 | - """Fake the session bus.""" |
705 | +class FakedSSOService(object): |
706 | + """A faked SSO service.""" |
707 | |
708 | def __init__(self): |
709 | - """Init.""" |
710 | - self.obj = None |
711 | - self.callbacks = {} |
712 | - |
713 | - def add_signal_receiver(self, method, signal_name, dbus_interface): |
714 | - """Add a signal receiver.""" |
715 | - self.callbacks[(dbus_interface, signal_name)] = method |
716 | - |
717 | - def remove_signal_receiver(self, match, signal_name, dbus_interface): |
718 | - """Remove the signal receiver.""" |
719 | - assert (dbus_interface, signal_name) in self.callbacks |
720 | - del self.callbacks[(dbus_interface, signal_name)] |
721 | - |
722 | - def get_object(self, bus_name, object_path, introspect=True, |
723 | - follow_name_owner_changes=False, **kwargs): |
724 | - """Return a faked proxy for the given remote object.""" |
725 | - if bus_name == gui.DBUS_BUS_NAME and \ |
726 | - object_path == gui.DBUS_ACCOUNT_PATH: |
727 | - assert self.obj is None |
728 | - kwargs = dict(object_path=object_path, |
729 | - bus_name=bus_name, follow_name_owner_changes=True) |
730 | - self.obj = FakedDbusObject(**kwargs) |
731 | - return self.obj |
732 | + self.sso_login = FakedSSOBackend() |
733 | |
734 | |
735 | class Settings(dict): |
736 | @@ -165,6 +146,8 @@ |
737 | yield super(BasicTestCase, self).setUp() |
738 | self._called = False # helper |
739 | |
740 | + self.patch(gui.sys, 'exit', lambda *a: None) |
741 | + |
742 | self.memento = MementoHandler() |
743 | self.memento.setLevel(logging.DEBUG) |
744 | gui.logger.addHandler(self.memento) |
745 | @@ -382,20 +365,14 @@ |
746 | def setUp(self): |
747 | """Init.""" |
748 | yield super(UbuntuSSOClientTestCase, self).setUp() |
749 | - self.patch(dbus, 'SessionBus', FakedSessionBus) |
750 | - self.patch(dbus, 'Interface', FakedSSOBackend) |
751 | + self.patch(gui.main, 'get_sso_client', |
752 | + lambda: defer.succeed(FakedSSOService())) |
753 | self.pages = ('enter_details', 'processing', 'verify_email', 'finish', |
754 | 'tc_browser', 'login', 'request_password_token', |
755 | 'set_new_password') |
756 | self.ui = self.gui_class(**self.kwargs) |
757 | self.error = {'message': UNKNOWN_ERROR} |
758 | |
759 | - @defer.inlineCallbacks |
760 | - def tearDown(self): |
761 | - self.ui.bus.callbacks = {} |
762 | - self.ui = None |
763 | - yield super(UbuntuSSOClientTestCase, self).tearDown() |
764 | - |
765 | def assert_entries_are_packed_to_ui(self, container_name, entries): |
766 | """Every entry is properly packed in the ui 'container_name'.""" |
767 | msg = 'Entry "%s" must be packed in "%s" but is not.' |
768 | @@ -524,39 +501,13 @@ |
769 | """The app_name is stored for further use.""" |
770 | self.assertIn(APP_NAME, self.ui.app_name) |
771 | |
772 | - def test_session_bus_is_correct(self): |
773 | - """The session bus is created and is correct.""" |
774 | - self.assertIsInstance(self.ui.bus, FakedSessionBus) |
775 | - |
776 | - def test_iface_name_is_correct(self): |
777 | - """The session bus is created and is correct.""" |
778 | - self.assertEqual(self.ui.iface_name, gui.DBUS_IFACE_USER_NAME) |
779 | - |
780 | - def test_bus_object_is_created(self): |
781 | - """SessionBus.get_object is called properly.""" |
782 | - self.assertIsInstance(self.ui.bus.obj, FakedDbusObject) |
783 | - self.assertEqual(self.ui.bus.obj._args, ()) |
784 | - expected = dict(object_path=gui.DBUS_ACCOUNT_PATH, |
785 | - bus_name=gui.DBUS_BUS_NAME, |
786 | - follow_name_owner_changes=True) |
787 | - self.assertEqual(expected, self.ui.bus.obj._kwargs) |
788 | - |
789 | - def test_bus_interface_is_created(self): |
790 | - """dbus.Interface is called properly.""" |
791 | - self.assertIsInstance(self.ui.backend, FakedSSOBackend) |
792 | - self.assertEqual(self.ui.backend._args, ()) |
793 | - expected = dict(object=self.ui.bus.obj, |
794 | - dbus_interface=gui.DBUS_IFACE_USER_NAME) |
795 | - self.assertEqual(expected, self.ui.backend._kwargs) |
796 | - |
797 | - def test_dbus_signals_are_removed(self): |
798 | + def test_signals_are_removed(self): |
799 | """The hooked signals are removed at shutdown time.""" |
800 | - self.ui._setup_signals() |
801 | - assert len(self.ui.bus.callbacks) > 0 # at least one callback |
802 | + assert len(self.ui.backend.callbacks) > 0 # at least one callback |
803 | |
804 | self.ui.on_close_clicked() |
805 | |
806 | - self.assertEqual(self.ui.bus.callbacks, {}) |
807 | + self.assertEqual(self.ui.backend.callbacks, {}) |
808 | |
809 | def test_pages_are_packed_into_container(self): |
810 | """All the pages are packed in the main container.""" |
811 | @@ -607,6 +558,7 @@ |
812 | |
813 | def test_cancel_buttons_close_window(self): |
814 | """Every cancel button should close the window when clicked.""" |
815 | + self.patch(self.ui.backend, 'disconnect_from_signal', lambda *a: None) |
816 | msg = '"%s" should close the window when clicked.' |
817 | buttons = filter(lambda name: 'cancel_button' in name or |
818 | 'close_button' in name, self.ui.widgets) |
819 | @@ -923,15 +875,12 @@ |
820 | self.patch(webkit, 'WebView', FakedEmbeddedBrowser) |
821 | |
822 | self.ui.tc_button.clicked() |
823 | + self.addCleanup(self.ui.tc_browser_vbox.hide) |
824 | + |
825 | children = self.ui.tc_browser_window.get_children() |
826 | assert len(children) == 1 |
827 | self.browser = children[0] |
828 | |
829 | - @defer.inlineCallbacks |
830 | - def tearDown(self): |
831 | - self.ui.tc_browser_vbox.hide() |
832 | - yield super(TermsAndConditionsBrowserTestCase, self).tearDown() |
833 | - |
834 | def test_tc_browser_is_created_when_tc_page_is_shown(self): |
835 | """The browser is created when the TC button is clicked.""" |
836 | self.ui.on_tc_browser_notify_load_status(self.browser) |
837 | @@ -1124,12 +1073,14 @@ |
838 | class VerifyEmailTestCase(UbuntuSSOClientTestCase): |
839 | """Test suite for the user registration (verify email page).""" |
840 | |
841 | + method = 'validate_email' |
842 | + method_args = (APP_NAME, EMAIL, PASSWORD, EMAIL_TOKEN) |
843 | + |
844 | @defer.inlineCallbacks |
845 | def setUp(self): |
846 | """Init.""" |
847 | yield super(VerifyEmailTestCase, self).setUp() |
848 | self.ui.on_user_registered(app_name=APP_NAME, email=EMAIL) |
849 | - self.click_verify_email_with_valid_data() |
850 | |
851 | def test_registration_successful_shows_verify_email_vbox(self): |
852 | """Receiving 'registration_successful' shows the verify email vbox.""" |
853 | @@ -1144,13 +1095,13 @@ |
854 | 'email': EMAIL} |
855 | self.assertEqual(expected, actual, msg % (expected, actual)) |
856 | |
857 | - def test_on_verify_token_button_clicked_calls_validate_email(self): |
858 | + def test_on_verify_token_button_clicked_calls_backend(self): |
859 | """Verify token button triggers call to backend.""" |
860 | self.click_verify_email_with_valid_data() |
861 | - expected = 'validate_email' |
862 | + expected = self.method |
863 | self.assertIn(expected, self.ui.backend._called) |
864 | self.assertEqual(self.ui.backend._called[expected], |
865 | - ((APP_NAME, EMAIL, PASSWORD, EMAIL_TOKEN), |
866 | + (self.method_args, |
867 | dict(reply_handler=gui.NO_OP, |
868 | error_handler=gui.NO_OP))) |
869 | |
870 | @@ -1158,10 +1109,16 @@ |
871 | """Verify token uses cached user_email and user_password.""" |
872 | self.ui.user_email = 'test@me.com' |
873 | self.ui.user_password = 'yadda-yedda' |
874 | + method_args = list(self.method_args) |
875 | + method_args[1] = self.ui.user_email |
876 | + method_args[2] = self.ui.user_password |
877 | + |
878 | + # resolve email token properly |
879 | + self.ui.email_token_entry.set_text(EMAIL_TOKEN) |
880 | + |
881 | self.ui.on_verify_token_button_clicked() |
882 | - self.assertEqual(self.ui.backend._called['validate_email'], |
883 | - ((APP_NAME, self.ui.user_email, |
884 | - self.ui.user_password, EMAIL_TOKEN), |
885 | + self.assertEqual(self.ui.backend._called[self.method], |
886 | + (tuple(method_args), |
887 | dict(reply_handler=gui.NO_OP, |
888 | error_handler=gui.NO_OP))) |
889 | |
890 | @@ -1240,31 +1197,22 @@ |
891 | self.ui.verify_token_button.clicked() |
892 | self.assertTrue(self._called) |
893 | |
894 | - def test_after_registration_success_finish_success(self): |
895 | - """After REGISTRATION_SUCCESS is called, finish_success is called. |
896 | - |
897 | - Only when REGISTRATION_SUCCESS returns 0. |
898 | - |
899 | - """ |
900 | + def test_after_email_validated_finish_success(self): |
901 | + """After email_validated is called, finish_success is called.""" |
902 | self.patch(self.ui, 'finish_success', self._set_called) |
903 | - self.ui.registration_success_callback = lambda app, email: 0 |
904 | - |
905 | - self.ui.on_email_validated(app_name=APP_NAME, email=EMAIL) |
906 | - |
907 | - self.assertEqual(self._called, ((), {})) |
908 | - |
909 | - def test_after_registration_error_finish_error(self): |
910 | - """After REGISTRATION_SUCCESS is called, finish_error is called. |
911 | - |
912 | - Only when REGISTRATION_SUCCESS returns a non-zero value. |
913 | - |
914 | - """ |
915 | - self.patch(self.ui, 'finish_error', self._set_called) |
916 | - self.ui.registration_success_callback = lambda app, email: -1 |
917 | - |
918 | - self.ui.on_email_validated(app_name=APP_NAME, email=EMAIL) |
919 | - |
920 | - self.assertEqual(self._called, ((), {})) |
921 | + |
922 | + self.ui.on_email_validated(app_name=APP_NAME, email=EMAIL) |
923 | + |
924 | + self.assertEqual(self._called, ((), {})) |
925 | + |
926 | + |
927 | +class VerifyEmailWithPingTestCase(VerifyEmailTestCase): |
928 | + """Test suite for the user registration (verify email page).""" |
929 | + |
930 | + kwargs = dict(app_name=APP_NAME, tc_url=TC_URL, help_text=HELP_TEXT, |
931 | + ping_url=PING_URL) |
932 | + method = 'validate_email_with_ping' |
933 | + method_args = (APP_NAME, EMAIL, PASSWORD, EMAIL_TOKEN, PING_URL) |
934 | |
935 | |
936 | class VerifyEmailValidationTestCase(UbuntuSSOClientTestCase): |
937 | @@ -1455,6 +1403,9 @@ |
938 | class LoginTestCase(UbuntuSSOClientTestCase): |
939 | """Test suite for the user login pages.""" |
940 | |
941 | + method = 'login' |
942 | + method_args = (APP_NAME, EMAIL, PASSWORD) |
943 | + |
944 | @defer.inlineCallbacks |
945 | def setUp(self): |
946 | """Init.""" |
947 | @@ -1505,10 +1456,10 @@ |
948 | """Clicking login_ok_button calls backend.login.""" |
949 | self.click_connect_with_valid_data() |
950 | |
951 | - expected = 'login' |
952 | + expected = self.method |
953 | self.assertIn(expected, self.ui.backend._called) |
954 | self.assertEqual(self.ui.backend._called[expected], |
955 | - ((APP_NAME, EMAIL, PASSWORD), |
956 | + (self.method_args, |
957 | dict(reply_handler=gui.NO_OP, |
958 | error_handler=gui.NO_OP))) |
959 | |
960 | @@ -1579,30 +1530,21 @@ |
961 | self.assertEqual(self.ui.user_password, PASSWORD) |
962 | |
963 | def test_after_login_success_finish_success(self): |
964 | - """After LOGIN_SUCCESSFULL is called, finish_success is called. |
965 | - |
966 | - Only when LOGIN_SUCCESSFULL returns 0. |
967 | - |
968 | - """ |
969 | + """After logged_in is called, finish_success is called.""" |
970 | self.patch(self.ui, 'finish_success', self._set_called) |
971 | - self.ui.login_success_callback = lambda app, email: 0 |
972 | - |
973 | - self.ui.on_logged_in(app_name=APP_NAME, email=EMAIL) |
974 | - |
975 | - self.assertEqual(self._called, ((), {})) |
976 | - |
977 | - def test_after_login_error_finish_error(self): |
978 | - """After LOGIN_SUCCESSFULL is called, finish_error is called. |
979 | - |
980 | - Only when LOGIN_SUCCESSFULL returns a non-zero value. |
981 | - |
982 | - """ |
983 | - self.patch(self.ui, 'finish_error', self._set_called) |
984 | - self.ui.login_success_callback = lambda app, email: -1 |
985 | - |
986 | - self.ui.on_logged_in(app_name=APP_NAME, email=EMAIL) |
987 | - |
988 | - self.assertEqual(self._called, ((), {})) |
989 | + |
990 | + self.ui.on_logged_in(app_name=APP_NAME, email=EMAIL) |
991 | + |
992 | + self.assertEqual(self._called, ((), {})) |
993 | + |
994 | + |
995 | +class LoginWithPingTestCase(LoginTestCase): |
996 | + """Test suite for the login when the ping_url is set.""" |
997 | + |
998 | + kwargs = dict(app_name=APP_NAME, tc_url=TC_URL, help_text=HELP_TEXT, |
999 | + ping_url=PING_URL) |
1000 | + method = 'login_with_ping' |
1001 | + method_args = (APP_NAME, EMAIL, PASSWORD, PING_URL) |
1002 | |
1003 | |
1004 | class LoginValidationTestCase(UbuntuSSOClientTestCase): |
1005 | @@ -1978,8 +1920,8 @@ |
1006 | self.assert_warnings_visibility() |
1007 | |
1008 | |
1009 | -class DbusTestCase(UbuntuSSOClientTestCase): |
1010 | - """Test suite for the dbus calls.""" |
1011 | +class SignalsTestCase(UbuntuSSOClientTestCase): |
1012 | + """Test suite for the backend signals.""" |
1013 | |
1014 | def test_all_the_signals_are_listed(self): |
1015 | """All the backend signals are listed to be binded.""" |
1016 | @@ -1993,13 +1935,12 @@ |
1017 | |
1018 | def test_signal_receivers_are_connected(self): |
1019 | """Callbacks are connected to signals of interest.""" |
1020 | - msg1 = 'callback %r for signal %r must be added to the internal bus.' |
1021 | + msg1 = 'callback %r for signal %r must be added to the backend.' |
1022 | msg2 = 'callback %r for signal %r must be added to the ui log.' |
1023 | - dbus_iface = self.ui.iface_name |
1024 | for signal, method in self.ui._signals.iteritems(): |
1025 | - actual = self.ui.bus.callbacks.get((dbus_iface, signal)) |
1026 | - self.assertEqual(method, actual, msg1 % (method, signal)) |
1027 | - actual = self.ui._signals_receivers.get((dbus_iface, signal)) |
1028 | + actual = self.ui.backend.callbacks.get(signal) |
1029 | + self.assertEqual([method], actual, msg1 % (method, signal)) |
1030 | + actual = self.ui._signals_receivers.get(signal) |
1031 | self.assertEqual(method, actual, msg2 % (method, signal)) |
1032 | |
1033 | def test_callbacks_only_log_when_app_name_doesnt_match(self): |
1034 | @@ -2126,79 +2067,62 @@ |
1035 | self.assertEqual(self.ui.help_label.get_text(), HELP_TEXT) |
1036 | |
1037 | |
1038 | -class CallbacksTestCase(UbuntuSSOClientTestCase): |
1039 | - """Test the GTK callback calls.""" |
1040 | +class ReturnCodeTestCase(UbuntuSSOClientTestCase): |
1041 | + """Test the return codes.""" |
1042 | |
1043 | - LOGIN_SUCCESSFULL = 'login_success_callback' |
1044 | - REGISTRATION_SUCCESS = 'registration_success_callback' |
1045 | - USER_CANCELLATION = 'user_cancellation_callback' |
1046 | + LOGIN_SUCCESS = gui.LOGIN_SUCCESS |
1047 | + REGISTRATION_SUCCESS = gui.REGISTRATION_SUCCESS |
1048 | + USER_CANCELLATION = gui.USER_CANCELLATION |
1049 | |
1050 | @defer.inlineCallbacks |
1051 | def setUp(self): |
1052 | - """Init.""" |
1053 | - yield super(CallbacksTestCase, self).setUp() |
1054 | - self._called = {} |
1055 | - for name in (self.LOGIN_SUCCESSFULL, |
1056 | - self.REGISTRATION_SUCCESS, |
1057 | - self.USER_CANCELLATION): |
1058 | - setattr(self.ui, name, self._set_called(name)) |
1059 | - |
1060 | - def _set_called(self, callback_name): |
1061 | - """Keep trace of callbacks calls.""" |
1062 | - |
1063 | - # pylint: disable=W0221 |
1064 | - |
1065 | - def inner(*args, **kwargs): |
1066 | - """Store arguments used in this call.""" |
1067 | - self._called[callback_name] = args |
1068 | - |
1069 | - return inner |
1070 | + yield super(ReturnCodeTestCase, self).setUp() |
1071 | + self.patch(gui.sys, 'exit', self._set_called) |
1072 | |
1073 | def test_closing_main_window(self): |
1074 | """When closing the main window, USER_CANCELLATION is called.""" |
1075 | self.ui.window.emit('delete-event', gtk.gdk.Event(gtk.gdk.DELETE)) |
1076 | - self.assertEqual(self._called[self.USER_CANCELLATION], (APP_NAME,)) |
1077 | + self.assertEqual(self._called, ((gui.USER_CANCELLATION,), {})) |
1078 | |
1079 | def test_every_cancel_calls_proper_callback(self): |
1080 | """When any cancel button is clicked, USER_CANCELLATION is called.""" |
1081 | - msg = 'user_cancellation_callback is called when "%s" is clicked.' |
1082 | + self.patch(self.ui.backend, 'disconnect_from_signal', lambda *a: None) |
1083 | + msg = 'USER_CANCELLATION should be returned when "%s" is clicked.' |
1084 | buttons = filter(lambda name: 'cancel_button' in name, self.ui.widgets) |
1085 | for name in buttons: |
1086 | widget = getattr(self.ui, name) |
1087 | widget.clicked() |
1088 | - self.assertEqual(self._called[self.USER_CANCELLATION], (APP_NAME,), |
1089 | + self.assertEqual(self._called, ((gui.USER_CANCELLATION,), {}), |
1090 | msg % name) |
1091 | - self._called = {} |
1092 | + self._called = False |
1093 | |
1094 | def test_on_user_registration_error_proper_callback_is_called(self): |
1095 | """On UserRegistrationError, USER_CANCELLATION is called.""" |
1096 | self.ui.on_user_registration_error(app_name=APP_NAME, error=self.error) |
1097 | self.ui.on_close_clicked() |
1098 | |
1099 | - self.assertEqual(self._called[self.USER_CANCELLATION], (APP_NAME,)) |
1100 | + self.assertEqual(self._called, ((gui.USER_CANCELLATION,), {})) |
1101 | |
1102 | def test_on_email_validated_proper_callback_is_called(self): |
1103 | """On EmailValidated, REGISTRATION_SUCCESS is called.""" |
1104 | self.ui.on_email_validated(app_name=APP_NAME, email=EMAIL) |
1105 | self.ui.on_close_clicked() |
1106 | |
1107 | - self.assertEqual(self._called[self.REGISTRATION_SUCCESS], |
1108 | - (APP_NAME, EMAIL)) |
1109 | + self.assertEqual(self._called, ((gui.REGISTRATION_SUCCESS,), {})) |
1110 | |
1111 | def test_on_email_validation_error_proper_callback_is_called(self): |
1112 | """On EmailValidationError, USER_CANCELLATION is called.""" |
1113 | self.ui.on_email_validation_error(app_name=APP_NAME, error=self.error) |
1114 | self.ui.on_close_clicked() |
1115 | |
1116 | - self.assertEqual(self._called[self.USER_CANCELLATION], (APP_NAME,)) |
1117 | + self.assertEqual(self._called, ((gui.USER_CANCELLATION,), {})) |
1118 | |
1119 | def test_on_logged_in_proper_callback_is_called(self): |
1120 | - """On LoggedIn, LOGIN_SUCCESSFULL is called.""" |
1121 | + """On LoggedIn, LOGIN_SUCCESS is called.""" |
1122 | self.ui.on_logged_in(app_name=APP_NAME, email=EMAIL) |
1123 | self.ui.on_close_clicked() |
1124 | |
1125 | - self.assertEqual(self._called[self.LOGIN_SUCCESSFULL], |
1126 | - (APP_NAME, EMAIL)) |
1127 | + self.assertEqual(self._called, ((gui.LOGIN_SUCCESS,), {})) |
1128 | |
1129 | def test_on_login_error_proper_callback_is_called(self): |
1130 | """On LoginError, USER_CANCELLATION is called.""" |
1131 | @@ -2206,7 +2130,7 @@ |
1132 | self.ui.on_login_error(app_name=APP_NAME, error=self.error) |
1133 | self.ui.on_close_clicked() |
1134 | |
1135 | - self.assertEqual(self._called[self.USER_CANCELLATION], (APP_NAME,)) |
1136 | + self.assertEqual(self._called, ((gui.USER_CANCELLATION,), {})) |
1137 | |
1138 | def test_registration_success_even_if_prior_registration_error(self): |
1139 | """Only one callback is called with the final outcome. |
1140 | @@ -2221,13 +2145,12 @@ |
1141 | self.ui.on_email_validated(app_name=APP_NAME, email=EMAIL) |
1142 | self.ui.on_close_clicked() |
1143 | |
1144 | - self.assertEqual(self._called[self.REGISTRATION_SUCCESS], |
1145 | - (APP_NAME, EMAIL)) |
1146 | + self.assertEqual(self._called, ((gui.REGISTRATION_SUCCESS,), {})) |
1147 | |
1148 | def test_login_success_even_if_prior_login_error(self): |
1149 | """Only one callback is called with the final outcome. |
1150 | |
1151 | - When the user successfully logins, LOGIN_SUCCESSFULL is called even if |
1152 | + When the user successfully logins, LOGIN_SUCCESS is called even if |
1153 | there were errors before. |
1154 | |
1155 | """ |
1156 | @@ -2237,8 +2160,7 @@ |
1157 | self.ui.on_logged_in(app_name=APP_NAME, email=EMAIL) |
1158 | self.ui.on_close_clicked() |
1159 | |
1160 | - self.assertEqual(self._called[self.LOGIN_SUCCESSFULL], |
1161 | - (APP_NAME, EMAIL)) |
1162 | + self.assertEqual(self._called, ((gui.LOGIN_SUCCESS,), {})) |
1163 | |
1164 | def test_user_cancelation_even_if_prior_registration_error(self): |
1165 | """Only one callback is called with the final outcome. |
1166 | @@ -2251,7 +2173,7 @@ |
1167 | self.ui.on_user_registration_error(app_name=APP_NAME, error=self.error) |
1168 | self.ui.join_cancel_button.clicked() |
1169 | |
1170 | - self.assertEqual(self._called[self.USER_CANCELLATION], (APP_NAME,)) |
1171 | + self.assertEqual(self._called, ((gui.USER_CANCELLATION,), {})) |
1172 | |
1173 | def test_user_cancelation_even_if_prior_login_error(self): |
1174 | """Only one callback is called with the final outcome. |
1175 | @@ -2264,7 +2186,7 @@ |
1176 | self.ui.on_login_error(app_name=APP_NAME, error=self.error) |
1177 | self.ui.login_cancel_button.clicked() |
1178 | |
1179 | - self.assertEqual(self._called[self.USER_CANCELLATION], (APP_NAME,)) |
1180 | + self.assertEqual(self._called, ((gui.USER_CANCELLATION,), {})) |
1181 | |
1182 | |
1183 | class DefaultButtonsTestCase(UbuntuSSOClientTestCase): |
1184 | |
1185 | === added file 'ubuntu_sso/gtk/tests/test_main.py' |
1186 | --- ubuntu_sso/gtk/tests/test_main.py 1970-01-01 00:00:00 +0000 |
1187 | +++ ubuntu_sso/gtk/tests/test_main.py 2012-01-31 21:52:24 +0000 |
1188 | @@ -0,0 +1,123 @@ |
1189 | +# -*- coding: utf-8 -*- |
1190 | +# |
1191 | +# Copyright 2012 Canonical Ltd. |
1192 | +# |
1193 | +# This program is free software: you can redistribute it and/or modify it |
1194 | +# under the terms of the GNU General Public License version 3, as published |
1195 | +# by the Free Software Foundation. |
1196 | +# |
1197 | +# This program is distributed in the hope that it will be useful, but |
1198 | +# WITHOUT ANY WARRANTY; without even the implied warranties of |
1199 | +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR |
1200 | +# PURPOSE. See the GNU General Public License for more details. |
1201 | +# |
1202 | +# You should have received a copy of the GNU General Public License along |
1203 | +# with this program. If not, see <http://www.gnu.org/licenses/>. |
1204 | + |
1205 | +"""Tests for the GUI main module.""" |
1206 | + |
1207 | +import sys |
1208 | + |
1209 | +from StringIO import StringIO |
1210 | + |
1211 | +from twisted.trial.unittest import TestCase |
1212 | + |
1213 | +from ubuntu_sso.gtk import main |
1214 | + |
1215 | + |
1216 | +APP_NAME = 'Foo Bar' |
1217 | +DEFAULTS = dict( |
1218 | + app_name=APP_NAME, |
1219 | + help_text='', |
1220 | + login_only=False, |
1221 | + ping_url='', |
1222 | + tc_url='', |
1223 | + window_id=0, |
1224 | +) |
1225 | +DUMMY_TEXT = """Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed |
1226 | +risus orci, lacinia ac tincidunt fermentum, vestibulum suscipit orci. Aliquam |
1227 | +quis aliquet magna. Morbi vitae ligula ut libero porttitor imperdiet. |
1228 | +Vestibulum et ipsum sapien, pellentesque ultricies risus. Aenean in lectus |
1229 | +orci. Cras a lorem sollicitudin dui mollis varius. In hac habitasse platea |
1230 | +dictumst.""" |
1231 | +PROGRAM = 'foo-bar' |
1232 | +SOME_URL = 'http://example.com/foo/bar' |
1233 | + |
1234 | + |
1235 | +class BasicTestCase(TestCase): |
1236 | + """Test case with a helper tracker.""" |
1237 | + |
1238 | + def assert_parse_args_custom_field(self, option_name=None, |
1239 | + option_value=None): |
1240 | + """The args are correctly set when using a custom value.""" |
1241 | + expected = dict(DEFAULTS) |
1242 | + argv = [PROGRAM, '--app_name=%s' % APP_NAME] |
1243 | + |
1244 | + if option_name is not None: |
1245 | + if option_value is not None: |
1246 | + argv.append('--%s=%s' % (option_name, option_value)) |
1247 | + expected[option_name] = option_value |
1248 | + else: |
1249 | + argv.append('--%s' % (option_name,)) |
1250 | + expected[option_name] = True |
1251 | + |
1252 | + self.patch(sys, 'argv', argv) |
1253 | + |
1254 | + result = main.parse_args() |
1255 | + |
1256 | + # Access to a protected member, pylint: disable=W0212 |
1257 | + self.assertEqual(expected, dict(result._get_kwargs())) |
1258 | + |
1259 | + def test_main(self): |
1260 | + """Calling main.main() a UI instance is created.""" |
1261 | + called = [] |
1262 | + self.patch(main, 'UbuntuSSOClientGUI', |
1263 | + lambda **kw: called.append(('GUI', kw))) |
1264 | + self.patch(main.gtk, 'main', |
1265 | + lambda: called.append('gtk.main')) |
1266 | + |
1267 | + kwargs = dict(foo='foo', bar='bar', baz='yadda', yadda=0) |
1268 | + main.main(**kwargs) |
1269 | + |
1270 | + kwargs['close_callback'] = main.gtk.main_quit |
1271 | + self.assertEqual(called, [('GUI', kwargs), 'gtk.main']) |
1272 | + |
1273 | + def test_parse_args_app_name_is_required(self): |
1274 | + """If no app_name, show help and exit.""" |
1275 | + called = [] |
1276 | + stderr = StringIO() |
1277 | + self.addCleanup(stderr.close) |
1278 | + |
1279 | + self.patch(sys, 'argv', [PROGRAM]) |
1280 | + self.patch(sys, 'exit', called.append) |
1281 | + self.patch(sys, 'stderr', stderr) |
1282 | + |
1283 | + main.parse_args() |
1284 | + |
1285 | + self.assertEqual(called, [2]) |
1286 | + self.assertIn('usage: %s' % PROGRAM, stderr.getvalue()) |
1287 | + self.assertIn('app_name is required', stderr.getvalue()) |
1288 | + |
1289 | + def test_parse_args_defaults(self): |
1290 | + """The args are correctly set when using defaults.""" |
1291 | + self.assert_parse_args_custom_field() |
1292 | + |
1293 | + def test_parse_args_help_text(self): |
1294 | + """The args are correctly set when using a custom help_text.""" |
1295 | + self.assert_parse_args_custom_field('help_text', DUMMY_TEXT) |
1296 | + |
1297 | + def test_parse_args_ping_url(self): |
1298 | + """The args are correctly set when using a custom ping_url.""" |
1299 | + self.assert_parse_args_custom_field('ping_url', SOME_URL) |
1300 | + |
1301 | + def test_parse_args_tc_url(self): |
1302 | + """The args are correctly set when using a custom tc_url.""" |
1303 | + self.assert_parse_args_custom_field('tc_url', SOME_URL) |
1304 | + |
1305 | + def test_parse_args_window_id(self): |
1306 | + """The args are correctly set when using a custom window_id.""" |
1307 | + self.assert_parse_args_custom_field('window_id', 42) |
1308 | + |
1309 | + def test_parse_args_login_only(self): |
1310 | + """The args are correctly set when using a custom login_only.""" |
1311 | + self.assert_parse_args_custom_field('login_only') |
1312 | |
1313 | === modified file 'ubuntu_sso/keyring/linux.py' |
1314 | --- ubuntu_sso/keyring/linux.py 2011-03-03 15:19:29 +0000 |
1315 | +++ ubuntu_sso/keyring/linux.py 2012-01-31 21:52:24 +0000 |
1316 | @@ -37,7 +37,7 @@ |
1317 | try_old_credentials) |
1318 | |
1319 | |
1320 | -logger = setup_logging("ubuntu_sso.keyring") |
1321 | +logger = setup_logging("ubuntu_sso.keyring.linux") |
1322 | |
1323 | |
1324 | class Keyring(object): |
1325 | @@ -51,16 +51,15 @@ |
1326 | def _find_keyring_item(self, app_name, attr=None): |
1327 | """Return the keyring item or None if not found.""" |
1328 | if attr is None: |
1329 | - logger.debug("getting attr") |
1330 | attr = self._get_keyring_attr(app_name) |
1331 | - logger.debug("finding all items") |
1332 | + logger.debug("Finding all items for app_name %r.", app_name) |
1333 | items = yield self.service.search_items(attr) |
1334 | if len(items) == 0: |
1335 | # if no items found, return None |
1336 | - logger.debug("No items found") |
1337 | + logger.debug("No items found!") |
1338 | returnValue(None) |
1339 | |
1340 | - logger.debug("Returning first item found") |
1341 | + logger.debug("Returning first item found.") |
1342 | returnValue(items[0]) |
1343 | |
1344 | def _get_keyring_attr(self, app_name): |
1345 | @@ -84,44 +83,36 @@ |
1346 | @inlineCallbacks |
1347 | def _migrate_old_token_name(self, app_name): |
1348 | """Migrate credentials with old name, store them with new name.""" |
1349 | - logger.debug("getting keyring attr") |
1350 | + logger.debug("Migrating old token name.") |
1351 | attr = self._get_keyring_attr(app_name) |
1352 | - logger.debug("getting old token name") |
1353 | attr['token-name'] = get_old_token_name(app_name) |
1354 | - logger.debug("finding keyring item") |
1355 | item = yield self._find_keyring_item(app_name, attr=attr) |
1356 | if item is not None: |
1357 | - logger.debug("setting credentials") |
1358 | yield self.set_credentials(app_name, |
1359 | dict(urlparse.parse_qsl(item.secret))) |
1360 | - logger.debug("deleting old item") |
1361 | yield item.delete() |
1362 | |
1363 | - logger.debug("finding keyring item") |
1364 | result = yield self._find_keyring_item(app_name) |
1365 | - logger.debug("returning result value") |
1366 | returnValue(result) |
1367 | |
1368 | @inlineCallbacks |
1369 | def get_credentials(self, app_name): |
1370 | """A deferred with the secret of the SSO item in a dictionary.""" |
1371 | # If we have no attributes, return None |
1372 | - logger.debug("getting credentials") |
1373 | + logger.debug("Getting credentials for %r.", app_name) |
1374 | yield self.service.open_session() |
1375 | - logger.debug("calling find item") |
1376 | item = yield self._find_keyring_item(app_name) |
1377 | if item is None: |
1378 | - logger.debug("migrating token") |
1379 | item = yield self._migrate_old_token_name(app_name) |
1380 | |
1381 | if item is not None: |
1382 | - logger.debug("parsing secret") |
1383 | + logger.debug("Parsing secret.") |
1384 | secret = yield item.get_value() |
1385 | returnValue(dict(urlparse.parse_qsl(secret))) |
1386 | else: |
1387 | # if no item found, try getting the old credentials |
1388 | if app_name == U1_APP_NAME: |
1389 | - logger.debug("trying old credentials") |
1390 | + logger.debug("Trying old credentials for %r.", app_name) |
1391 | old_creds = yield try_old_credentials(app_name) |
1392 | returnValue(old_creds) |
1393 | # nothing was found |
1394 | |
1395 | === modified file 'ubuntu_sso/main/linux.py' |
1396 | --- ubuntu_sso/main/linux.py 2012-01-17 15:09:12 +0000 |
1397 | +++ ubuntu_sso/main/linux.py 2012-01-31 21:52:24 +0000 |
1398 | @@ -73,6 +73,7 @@ |
1399 | |
1400 | def __init__(self, root, *args, **kwargs): |
1401 | """Initiate the Login object.""" |
1402 | + # pylint: disable=E1002 |
1403 | super(SSOLoginProxy, self).__init__(*args, **kwargs) |
1404 | self.root = root |
1405 | |
1406 | @@ -202,6 +203,7 @@ |
1407 | """ |
1408 | |
1409 | def __init__(self, root, *args, **kwargs): |
1410 | + # pylint: disable=E1002 |
1411 | super(CredentialsManagementProxy, self).__init__(*args, **kwargs) |
1412 | self.root = root |
1413 | |
1414 | |
1415 | === modified file 'ubuntu_sso/networkstate/linux.py' |
1416 | --- ubuntu_sso/networkstate/linux.py 2011-11-11 19:20:59 +0000 |
1417 | +++ ubuntu_sso/networkstate/linux.py 2012-01-31 21:52:24 +0000 |
1418 | @@ -78,8 +78,6 @@ |
1419 | |
1420 | def call_result_cb(self, state): |
1421 | """Return the state thru the result callback.""" |
1422 | - if self.state_signal: |
1423 | - self.state_signal.remove() |
1424 | self.result_cb(state) |
1425 | |
1426 | def got_state(self, state): |
1427 | |
1428 | === modified file 'ubuntu_sso/networkstate/tests/test_linux.py' |
1429 | --- ubuntu_sso/networkstate/tests/test_linux.py 2011-11-11 19:20:59 +0000 |
1430 | +++ ubuntu_sso/networkstate/tests/test_linux.py 2012-01-31 21:52:24 +0000 |
1431 | @@ -4,7 +4,7 @@ |
1432 | # |
1433 | # Author: Alejandro J. Cura <alecu@canonical.com> |
1434 | # |
1435 | -# Copyright 2010 Canonical Ltd. |
1436 | +# Copyright 2010-2012 Canonical Ltd. |
1437 | # |
1438 | # This program is free software: you can redistribute it and/or modify it |
1439 | # under the terms of the GNU General Public License version 3, as published |
1440 | @@ -19,29 +19,35 @@ |
1441 | # with this program. If not, see <http://www.gnu.org/licenses/>. |
1442 | """Tests for the network state detection code.""" |
1443 | |
1444 | +from collections import defaultdict |
1445 | + |
1446 | from twisted.internet.defer import inlineCallbacks |
1447 | +from mocker import ARGS, KWARGS, ANY, MockerTestCase |
1448 | |
1449 | from ubuntu_sso.tests import TestCase |
1450 | -from ubuntu_sso.networkstate import (NetworkManagerState, |
1451 | - ONLINE, OFFLINE, UNKNOWN) |
1452 | +from ubuntu_sso.networkstate import ( |
1453 | + linux, |
1454 | + NetworkFailException, |
1455 | + NetworkManagerState, |
1456 | + ONLINE, OFFLINE, UNKNOWN, |
1457 | +) |
1458 | |
1459 | -from ubuntu_sso.networkstate import NetworkFailException |
1460 | -from ubuntu_sso.networkstate import linux |
1461 | from ubuntu_sso.networkstate.linux import (is_machine_connected, |
1462 | - DBUS_UNKNOWN_SERVICE, |
1463 | - NM_STATE_ASLEEP, |
1464 | - NM_STATE_ASLEEP_OLD, |
1465 | - NM_STATE_CONNECTING, |
1466 | - NM_STATE_CONNECTING_OLD, |
1467 | - NM_STATE_CONNECTED_OLD, |
1468 | - NM_STATE_CONNECTED_LOCAL, |
1469 | - NM_STATE_CONNECTED_SITE, |
1470 | - NM_STATE_CONNECTED_GLOBAL, |
1471 | - NM_STATE_DISCONNECTED, |
1472 | - NM_STATE_DISCONNECTED_OLD, |
1473 | - NM_STATE_UNKNOWN) |
1474 | - |
1475 | -from mocker import ARGS, KWARGS, ANY, MockerTestCase |
1476 | + DBUS_UNKNOWN_SERVICE, |
1477 | + NM_DBUS_INTERFACE, |
1478 | + NM_DBUS_OBJECTPATH, |
1479 | + NM_STATE_ASLEEP, |
1480 | + NM_STATE_ASLEEP_OLD, |
1481 | + NM_STATE_CONNECTING, |
1482 | + NM_STATE_CONNECTING_OLD, |
1483 | + NM_STATE_CONNECTED_OLD, |
1484 | + NM_STATE_CONNECTED_LOCAL, |
1485 | + NM_STATE_CONNECTED_SITE, |
1486 | + NM_STATE_CONNECTED_GLOBAL, |
1487 | + NM_STATE_DISCONNECTED, |
1488 | + NM_STATE_DISCONNECTED_OLD, |
1489 | + NM_STATE_UNKNOWN, |
1490 | +) |
1491 | |
1492 | |
1493 | class TestException(Exception): |
1494 | @@ -66,6 +72,57 @@ |
1495 | self.call_function(self.connection_state) |
1496 | |
1497 | |
1498 | +class FakeDBusMatch(object): |
1499 | + |
1500 | + """Fake a DBus match.""" |
1501 | + |
1502 | + def __init__(self, name, callback, interface): |
1503 | + self.name = name |
1504 | + self.callback = callback |
1505 | + self.interface = interface |
1506 | + self.removed = False |
1507 | + |
1508 | + def remove(self): |
1509 | + """Stop calling the handler function on remove.""" |
1510 | + self.removed = True |
1511 | + |
1512 | + |
1513 | +class FakeDBusInterface(object): |
1514 | + |
1515 | + """Fake DBus Interface.""" |
1516 | + |
1517 | + def __init__(self): |
1518 | + self._signals = defaultdict(list) |
1519 | + |
1520 | + # pylint: disable=C0103 |
1521 | + def Get(self, *args, **kwargs): |
1522 | + """Fake Get.""" |
1523 | + # pylint: enable=C0103 |
1524 | + |
1525 | + def connect_to_signal(self, signal_name, handler_function, dbus_interface): |
1526 | + """Fake connect_to_signal.""" |
1527 | + match = FakeDBusMatch(signal_name, handler_function, dbus_interface) |
1528 | + self._signals[signal_name].append(match) |
1529 | + return match |
1530 | + |
1531 | + def emit_signal(self, signal_name, state): |
1532 | + """Emit signal for network state change.""" |
1533 | + for match in self._signals[signal_name]: |
1534 | + match.callback(state) |
1535 | + |
1536 | + |
1537 | +class FakeSystemBus(object): |
1538 | + |
1539 | + """Fake SystemBus.""" |
1540 | + |
1541 | + objects = {(NM_DBUS_INTERFACE, NM_DBUS_OBJECTPATH, True): object()} |
1542 | + |
1543 | + def get_object(self, interface, object_path, follow_name_owner_changes): |
1544 | + """Fake get_object.""" |
1545 | + key = (interface, object_path, follow_name_owner_changes) |
1546 | + return self.objects[key] |
1547 | + |
1548 | + |
1549 | class TestConnection(TestCase): |
1550 | |
1551 | """Test the state of the connection. |
1552 | @@ -79,6 +136,34 @@ |
1553 | """Setup the mocker dbus object tree.""" |
1554 | yield super(TestConnection, self).setUp() |
1555 | self.patch(linux, "NetworkManagerState", FakeNetworkManagerState) |
1556 | + self.patch(linux.dbus, 'SystemBus', FakeSystemBus) |
1557 | + self.nm_interface = FakeDBusInterface() |
1558 | + self.patch(linux.dbus, 'Interface', lambda *a: self.nm_interface) |
1559 | + self.network_changes = [] |
1560 | + |
1561 | + def _listen_network_changes(self, state): |
1562 | + """Fake callback function.""" |
1563 | + self.network_changes.append(state) |
1564 | + |
1565 | + def test_network_state_change(self): |
1566 | + """Test the changes in the network connection.""" |
1567 | + nms = NetworkManagerState(self._listen_network_changes) |
1568 | + |
1569 | + nms.find_online_state() |
1570 | + |
1571 | + self.nm_interface.emit_signal('StateChanged', |
1572 | + NM_STATE_CONNECTED_GLOBAL) |
1573 | + self.nm_interface.emit_signal('StateChanged', NM_STATE_DISCONNECTED) |
1574 | + self.nm_interface.emit_signal('StateChanged', |
1575 | + NM_STATE_CONNECTED_GLOBAL) |
1576 | + |
1577 | + self.assertEqual(nms.state_signal.name, "StateChanged") |
1578 | + self.assertEqual(nms.state_signal.callback, nms.state_changed) |
1579 | + self.assertEqual(nms.state_signal.interface, |
1580 | + "org.freedesktop.NetworkManager") |
1581 | + self.assertEqual(self.network_changes, |
1582 | + [UNKNOWN, ONLINE, OFFLINE, ONLINE]) |
1583 | + self.assertFalse(nms.state_signal.removed) |
1584 | |
1585 | @inlineCallbacks |
1586 | def test_is_machine_connected_nm_state_connected_old(self): |
1587 | @@ -216,7 +301,6 @@ |
1588 | if exc is not None: |
1589 | self.expect(self.dbusmock.exceptions.DBusException) |
1590 | self.mocker.result(exc) |
1591 | - signalmock.remove() |
1592 | self.mocker.replay() |
1593 | |
1594 | # Invalid name "assert[A-Z].*" |
1595 | |
1596 | === modified file 'ubuntu_sso/utils/tests/test_txsecrets.py' |
1597 | --- ubuntu_sso/utils/tests/test_txsecrets.py 2012-01-17 15:09:12 +0000 |
1598 | +++ ubuntu_sso/utils/tests/test_txsecrets.py 2012-01-31 21:52:24 +0000 |
1599 | @@ -61,6 +61,7 @@ |
1600 | |
1601 | def __init__(self, collection, label, attributes, value, *args, **kwargs): |
1602 | """Initialize this instance.""" |
1603 | + # pylint: disable=E1002 |
1604 | super(ItemMock, self).__init__(*args, **kwargs) |
1605 | self.collection = collection |
1606 | self.label = label |
1607 | @@ -107,6 +108,7 @@ |
1608 | def __init__(self, dismissed=True, |
1609 | result=dbus.String("", variant_level=1), *args, **kwargs): |
1610 | """Initialize this instance.""" |
1611 | + # pylint: disable=E1002 |
1612 | super(PromptMock, self).__init__(*args, **kwargs) |
1613 | self.dismissed = dismissed |
1614 | self.result = result |
1615 | @@ -140,6 +142,7 @@ |
1616 | |
1617 | def __init__(self, label, *args, **kwargs): |
1618 | """Initialize this instance.""" |
1619 | + # pylint: disable=E1002 |
1620 | super(BaseCollectionMock, self).__init__(*args, **kwargs) |
1621 | self.items = [] |
1622 | self.label = label |
1623 | @@ -218,6 +221,7 @@ |
1624 | |
1625 | def __init__(self, *args, **kwargs): |
1626 | """Initialize this instance.""" |
1627 | + # pylint: disable=E1002 |
1628 | super(SecretServiceMock, self).__init__(*args, **kwargs) |
1629 | self.sessions = {} |
1630 | self.collections = {} |
1631 | @@ -377,6 +381,7 @@ |
1632 | in_signature="a{sv}s", out_signature="oo") |
1633 | def CreateCollection(self, properties, alias): |
1634 | """Create a new collection with the specified properties.""" |
1635 | + # pylint: disable=E1002 |
1636 | collection, prompt = super(AltSecretServiceMock, |
1637 | self).CreateCollection(properties) |
1638 | self.SetAlias(alias, collection) |
1639 | |
1640 | === modified file 'ubuntu_sso/utils/webclient/common.py' |
1641 | --- ubuntu_sso/utils/webclient/common.py 2012-01-17 15:09:12 +0000 |
1642 | +++ ubuntu_sso/utils/webclient/common.py 2012-01-31 21:52:24 +0000 |
1643 | @@ -15,11 +15,12 @@ |
1644 | # with this program. If not, see <http://www.gnu.org/licenses/>. |
1645 | """The common bits of a webclient.""" |
1646 | |
1647 | -import time |
1648 | +import collections |
1649 | |
1650 | from httplib2 import iri2uri |
1651 | from oauth import oauth |
1652 | -from twisted.internet import defer |
1653 | + |
1654 | +from ubuntu_sso.utils.webclient.timestamp import TimestampChecker |
1655 | |
1656 | |
1657 | class WebClientError(Exception): |
1658 | @@ -39,25 +40,60 @@ |
1659 | self.headers = headers |
1660 | |
1661 | |
1662 | +class HeaderDict(collections.defaultdict): |
1663 | + """A case insensitive dict for headers.""" |
1664 | + |
1665 | + # pylint: disable=E1002 |
1666 | + def __init__(self, *args, **kwargs): |
1667 | + """Handle case-insensitive keys.""" |
1668 | + super(HeaderDict, self).__init__(list, *args, **kwargs) |
1669 | + # pylint: disable=E1101 |
1670 | + for key, value in self.items(): |
1671 | + super(HeaderDict, self).__delitem__(key) |
1672 | + self[key] = value |
1673 | + |
1674 | + def __setitem__(self, key, value): |
1675 | + """Set the value with a case-insensitive key.""" |
1676 | + super(HeaderDict, self).__setitem__(key.lower(), value) |
1677 | + |
1678 | + def __getitem__(self, key): |
1679 | + """Get the value with a case-insensitive key.""" |
1680 | + return super(HeaderDict, self).__getitem__(key.lower()) |
1681 | + |
1682 | + def __delitem__(self, key): |
1683 | + """Delete the item with the case-insensitive key.""" |
1684 | + super(HeaderDict, self).__delitem__(key.lower()) |
1685 | + |
1686 | + def __contains__(self, key): |
1687 | + """Check the containment with a case-insensitive key.""" |
1688 | + return super(HeaderDict, self).__contains__(key.lower()) |
1689 | + |
1690 | + |
1691 | class BaseWebClient(object): |
1692 | """The webclient base class, to be extended by backends.""" |
1693 | |
1694 | + timestamp_checker = None |
1695 | + |
1696 | def __init__(self, username=None, password=None): |
1697 | """Initialize this instance.""" |
1698 | self.username = username |
1699 | self.password = password |
1700 | |
1701 | def request(self, iri, method="GET", extra_headers=None, |
1702 | - oauth_credentials=None): |
1703 | + oauth_credentials=None, post_content=None): |
1704 | """Return a deferred that will be fired with a Response object.""" |
1705 | raise NotImplementedError |
1706 | |
1707 | + @classmethod |
1708 | + def get_timestamp_checker(cls): |
1709 | + """Get the timestamp checker for this class of webclient.""" |
1710 | + if cls.timestamp_checker is None: |
1711 | + cls.timestamp_checker = TimestampChecker(cls()) |
1712 | + return cls.timestamp_checker |
1713 | + |
1714 | def get_timestamp(self): |
1715 | """Get a timestamp synchronized with the server.""" |
1716 | - # pylint: disable=W0511 |
1717 | - # TODO: get the synchronized timestamp |
1718 | - timestamp = time.time() |
1719 | - return defer.succeed(int(timestamp)) |
1720 | + return self.get_timestamp_checker().get_faithful_time() |
1721 | |
1722 | def force_use_proxy(self, settings): |
1723 | """Setup this webclient to use the given proxy settings.""" |
1724 | |
1725 | === modified file 'ubuntu_sso/utils/webclient/libsoup.py' |
1726 | --- ubuntu_sso/utils/webclient/libsoup.py 2012-01-17 15:09:12 +0000 |
1727 | +++ ubuntu_sso/utils/webclient/libsoup.py 2012-01-31 21:52:24 +0000 |
1728 | @@ -17,12 +17,11 @@ |
1729 | |
1730 | import httplib |
1731 | |
1732 | -from collections import defaultdict |
1733 | - |
1734 | from twisted.internet import defer |
1735 | |
1736 | from ubuntu_sso.utils.webclient.common import ( |
1737 | BaseWebClient, |
1738 | + HeaderDict, |
1739 | Response, |
1740 | UnauthorizedError, |
1741 | WebClientError, |
1742 | @@ -48,7 +47,7 @@ |
1743 | def _on_message(self, session, message, d): |
1744 | """Handle the result of an http message.""" |
1745 | if message.status_code == httplib.OK: |
1746 | - headers = defaultdict(list) |
1747 | + headers = HeaderDict() |
1748 | response_headers = message.get_property("response-headers") |
1749 | add_header = lambda key, value, _: headers[key].append(value) |
1750 | response_headers.foreach(add_header, None) |
1751 | @@ -68,7 +67,7 @@ |
1752 | |
1753 | @defer.inlineCallbacks |
1754 | def request(self, iri, method="GET", extra_headers=None, |
1755 | - oauth_credentials=None): |
1756 | + oauth_credentials=None, post_content=None): |
1757 | """Return a deferred that will be fired with a Response object.""" |
1758 | uri = self.iri_to_uri(iri) |
1759 | if extra_headers: |
1760 | @@ -88,6 +87,9 @@ |
1761 | for key, value in headers.iteritems(): |
1762 | message.request_headers.append(key, value) |
1763 | |
1764 | + if post_content: |
1765 | + message.request_body.append(post_content) |
1766 | + |
1767 | self.session.queue_message(message, self._on_message, d) |
1768 | response = yield d |
1769 | defer.returnValue(response) |
1770 | |
1771 | === modified file 'ubuntu_sso/utils/webclient/qtnetwork.py' |
1772 | --- ubuntu_sso/utils/webclient/qtnetwork.py 2012-01-17 15:09:12 +0000 |
1773 | +++ ubuntu_sso/utils/webclient/qtnetwork.py 2012-01-31 21:52:24 +0000 |
1774 | @@ -17,9 +17,8 @@ |
1775 | |
1776 | import sys |
1777 | |
1778 | -from collections import defaultdict |
1779 | - |
1780 | from PyQt4.QtCore import ( |
1781 | + QBuffer, |
1782 | QCoreApplication, |
1783 | QUrl, |
1784 | ) |
1785 | @@ -33,6 +32,7 @@ |
1786 | |
1787 | from ubuntu_sso.utils.webclient.common import ( |
1788 | BaseWebClient, |
1789 | + HeaderDict, |
1790 | Response, |
1791 | UnauthorizedError, |
1792 | WebClientError, |
1793 | @@ -62,7 +62,7 @@ |
1794 | |
1795 | @defer.inlineCallbacks |
1796 | def request(self, iri, method="GET", extra_headers=None, |
1797 | - oauth_credentials=None): |
1798 | + oauth_credentials=None, post_content=None): |
1799 | """Return a deferred that will be fired with a Response object.""" |
1800 | uri = self.iri_to_uri(iri) |
1801 | request = QNetworkRequest(QUrl(uri)) |
1802 | @@ -87,7 +87,9 @@ |
1803 | elif method == "HEAD": |
1804 | reply = self.nam.head(request) |
1805 | else: |
1806 | - reply = self.nam.sendCustomRequest(request, method) |
1807 | + post_buffer = QBuffer() |
1808 | + post_buffer.setData(post_content) |
1809 | + reply = self.nam.sendCustomRequest(request, method, post_buffer) |
1810 | self.replies[reply] = d |
1811 | result = yield d |
1812 | defer.returnValue(result) |
1813 | @@ -104,7 +106,7 @@ |
1814 | error = reply.error() |
1815 | content = reply.readAll() |
1816 | if not error: |
1817 | - headers = defaultdict(list) |
1818 | + headers = HeaderDict() |
1819 | for key, value in reply.rawHeaderPairs(): |
1820 | headers[str(key)].append(str(value)) |
1821 | response = Response(bytes(content), headers) |
1822 | |
1823 | === modified file 'ubuntu_sso/utils/webclient/restful.py' |
1824 | --- ubuntu_sso/utils/webclient/restful.py 2012-01-06 18:57:43 +0000 |
1825 | +++ ubuntu_sso/utils/webclient/restful.py 2012-01-31 21:52:24 +0000 |
1826 | @@ -22,6 +22,10 @@ |
1827 | |
1828 | from ubuntu_sso.utils import webclient |
1829 | |
1830 | +POST_HEADERS = { |
1831 | + "content-type": "application/x-www-form-urlencoded", |
1832 | +} |
1833 | + |
1834 | |
1835 | class RestfulClient(object): |
1836 | """A proxy-enabled restful client.""" |
1837 | @@ -39,14 +43,18 @@ |
1838 | def restcall(self, method, **kwargs): |
1839 | """Make a restful call.""" |
1840 | assert isinstance(method, unicode) |
1841 | + params = {} |
1842 | for key, value in kwargs.items(): |
1843 | if isinstance(value, basestring): |
1844 | assert isinstance(value, unicode) |
1845 | - kwargs[key] = value.encode("utf-8") |
1846 | + params[key] = json.dumps(value) |
1847 | namespace, operation = method.split(".") |
1848 | - kwargs["ws.op"] = operation |
1849 | - encoded_args = urllib.urlencode(kwargs) |
1850 | - iri = self.service_iri + namespace + "?" + encoded_args |
1851 | + params["ws.op"] = operation |
1852 | + encoded_args = urllib.urlencode(params) |
1853 | + iri = self.service_iri + namespace |
1854 | creds = self.oauth_credentials |
1855 | - result = yield self.webclient.request(iri, oauth_credentials=creds) |
1856 | + result = yield self.webclient.request(iri, method="POST", |
1857 | + oauth_credentials=creds, |
1858 | + post_content=encoded_args, |
1859 | + extra_headers=POST_HEADERS) |
1860 | defer.returnValue(json.loads(result.content)) |
1861 | |
1862 | === modified file 'ubuntu_sso/utils/webclient/tests/test_restful.py' |
1863 | --- ubuntu_sso/utils/webclient/tests/test_restful.py 2012-01-06 18:57:43 +0000 |
1864 | +++ ubuntu_sso/utils/webclient/tests/test_restful.py 2012-01-31 21:52:24 +0000 |
1865 | @@ -87,40 +87,51 @@ |
1866 | self.assertEqual(len(self.wc.called), 1) |
1867 | |
1868 | @defer.inlineCallbacks |
1869 | - def get_parsed_url(self): |
1870 | - """Call the sample operation, and return the url used.""" |
1871 | + def test_restful_namespace_added_to_url(self): |
1872 | + """The restful namespace is added to the url.""" |
1873 | yield self.rc.restcall(SAMPLE_OPERATION, **SAMPLE_ARGS) |
1874 | iri, _, _ = self.wc.called[0] |
1875 | uri = iri.encode("ascii") |
1876 | - defer.returnValue(urlparse.urlparse(uri)) |
1877 | - |
1878 | - @defer.inlineCallbacks |
1879 | - def test_restful_namespace_added_to_url(self): |
1880 | - """The restful namespace is added to the url.""" |
1881 | - url = yield self.get_parsed_url() |
1882 | + url = urlparse.urlparse(uri) |
1883 | + # pylint: disable=E1101 |
1884 | self.assertTrue(url.path.endswith(SAMPLE_NAMESPACE), |
1885 | "The namespace is included in url") |
1886 | |
1887 | @defer.inlineCallbacks |
1888 | - def test_restful_method_added_to_url(self): |
1889 | - """The restful method is added to the url.""" |
1890 | - url = yield self.get_parsed_url() |
1891 | - url_query = urlparse.parse_qs(url.query) |
1892 | - self.assertEqual(url_query["ws.op"][0], SAMPLE_METHOD) |
1893 | - |
1894 | - @defer.inlineCallbacks |
1895 | - def test_arguments_added_to_call(self): |
1896 | - """The keyword arguments are used in the called url.""" |
1897 | - url = yield self.get_parsed_url() |
1898 | - url_query = dict(urlparse.parse_qsl(url.query)) |
1899 | - del(url_query["ws.op"]) |
1900 | - expected = {} |
1901 | - for key, value in SAMPLE_ARGS.items(): |
1902 | - if isinstance(value, unicode): |
1903 | - expected[key] = value.encode("utf-8") |
1904 | - else: |
1905 | - expected[key] = str(value) |
1906 | - self.assertEqual(url_query, expected) |
1907 | + def test_restful_method_added_to_params(self): |
1908 | + """The restful method is added to the params.""" |
1909 | + yield self.rc.restcall(SAMPLE_OPERATION, **SAMPLE_ARGS) |
1910 | + _, _, webcall_kwargs = self.wc.called[0] |
1911 | + wc_params = urlparse.parse_qs(webcall_kwargs["post_content"]) |
1912 | + self.assertEqual(wc_params["ws.op"][0], SAMPLE_METHOD) |
1913 | + |
1914 | + @defer.inlineCallbacks |
1915 | + def test_arguments_added_as_json_to_webcall(self): |
1916 | + """The keyword arguments are used as json in the webcall.""" |
1917 | + yield self.rc.restcall(SAMPLE_OPERATION, **SAMPLE_ARGS) |
1918 | + _, _, webcall_kwargs = self.wc.called[0] |
1919 | + params = urlparse.parse_qsl(webcall_kwargs["post_content"]) |
1920 | + result = {} |
1921 | + for key, value in params: |
1922 | + if key == "ws.op": |
1923 | + continue |
1924 | + result[key] = restful.json.loads(value) |
1925 | + self.assertEqual(result, SAMPLE_ARGS) |
1926 | + |
1927 | + @defer.inlineCallbacks |
1928 | + def test_post_header_sent(self): |
1929 | + """A header is sent specifying the contents of the post.""" |
1930 | + yield self.rc.restcall(SAMPLE_OPERATION, **SAMPLE_ARGS) |
1931 | + _, _, webcall_kwargs = self.wc.called[0] |
1932 | + self.assertEqual(restful.POST_HEADERS, |
1933 | + webcall_kwargs["extra_headers"]) |
1934 | + |
1935 | + @defer.inlineCallbacks |
1936 | + def test_post_method_set(self): |
1937 | + """The method of the webcall is set to POST.""" |
1938 | + yield self.rc.restcall(SAMPLE_OPERATION, **SAMPLE_ARGS) |
1939 | + _, _, webcall_kwargs = self.wc.called[0] |
1940 | + self.assertEqual("POST", webcall_kwargs["method"]) |
1941 | |
1942 | @defer.inlineCallbacks |
1943 | def test_return_value_json_parsed(self): |
1944 | |
1945 | === added file 'ubuntu_sso/utils/webclient/tests/test_timestamp.py' |
1946 | --- ubuntu_sso/utils/webclient/tests/test_timestamp.py 1970-01-01 00:00:00 +0000 |
1947 | +++ ubuntu_sso/utils/webclient/tests/test_timestamp.py 2012-01-31 21:52:24 +0000 |
1948 | @@ -0,0 +1,140 @@ |
1949 | +# -*- coding: utf-8 -*- |
1950 | +# |
1951 | +# Copyright 2011-2012 Canonical Ltd. |
1952 | +# |
1953 | +# This program is free software: you can redistribute it and/or modify it |
1954 | +# under the terms of the GNU General Public License version 3, as published |
1955 | +# by the Free Software Foundation. |
1956 | +# |
1957 | +# This program is distributed in the hope that it will be useful, but |
1958 | +# WITHOUT ANY WARRANTY; without even the implied warranties of |
1959 | +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR |
1960 | +# PURPOSE. See the GNU General Public License for more details. |
1961 | +# |
1962 | +# You should have received a copy of the GNU General Public License along |
1963 | +# with this program. If not, see <http://www.gnu.org/licenses/>. |
1964 | +"""Tests for the timestamp sync classes.""" |
1965 | + |
1966 | +from twisted.application import internet, service |
1967 | +from twisted.internet import defer |
1968 | +from twisted.trial.unittest import TestCase |
1969 | +from twisted.web import server, resource |
1970 | + |
1971 | +from ubuntu_sso.utils.webclient import timestamp, webclient_factory |
1972 | + |
1973 | + |
1974 | +class FakedError(Exception): |
1975 | + """Stub to replace Request.error.""" |
1976 | + |
1977 | + |
1978 | +class RootResource(resource.Resource): |
1979 | + """A root resource that logs the number of calls.""" |
1980 | + |
1981 | + isLeaf = True |
1982 | + |
1983 | + def __init__(self, *args, **kwargs): |
1984 | + """Initialize this fake instance.""" |
1985 | + resource.Resource.__init__(self, *args, **kwargs) |
1986 | + self.count = 0 |
1987 | + self.request_headers = [] |
1988 | + |
1989 | + # pylint: disable=C0103 |
1990 | + def render_HEAD(self, request): |
1991 | + """Increase the counter on each render.""" |
1992 | + self.request_headers.append(request.requestHeaders) |
1993 | + self.count += 1 |
1994 | + return "" |
1995 | + |
1996 | + |
1997 | +class MockWebServer(object): |
1998 | + """A mock webserver for testing.""" |
1999 | + |
2000 | + # pylint: disable=E1101 |
2001 | + def __init__(self): |
2002 | + """Start up this instance.""" |
2003 | + self.root = RootResource() |
2004 | + site = server.Site(self.root) |
2005 | + application = service.Application('web') |
2006 | + self.service_collection = service.IServiceCollection(application) |
2007 | + self.tcpserver = internet.TCPServer(0, site) |
2008 | + self.tcpserver.setServiceParent(self.service_collection) |
2009 | + self.service_collection.startService() |
2010 | + |
2011 | + def get_iri(self): |
2012 | + """Build the url for this mock server.""" |
2013 | + # pylint: disable=W0212 |
2014 | + port_num = self.tcpserver._port.getHost().port |
2015 | + return u"http://localhost:%d/" % port_num |
2016 | + |
2017 | + def stop(self): |
2018 | + """Shut it down.""" |
2019 | + self.service_collection.stopService() |
2020 | + |
2021 | + |
2022 | +class TimestampCheckerTestCase(TestCase): |
2023 | + """Tests for the timestamp checker.""" |
2024 | + |
2025 | + @defer.inlineCallbacks |
2026 | + def setUp(self): |
2027 | + yield super(TimestampCheckerTestCase, self).setUp() |
2028 | + self.ws = MockWebServer() |
2029 | + self.addCleanup(self.ws.stop) |
2030 | + self.wc = webclient_factory() |
2031 | + self.addCleanup(self.wc.shutdown) |
2032 | + self.patch(timestamp.TimestampChecker, "SERVER_IRI", self.ws.get_iri()) |
2033 | + |
2034 | + @defer.inlineCallbacks |
2035 | + def test_returned_value_is_int(self): |
2036 | + """The returned value is an integer.""" |
2037 | + checker = timestamp.TimestampChecker(self.wc) |
2038 | + result = yield checker.get_faithful_time() |
2039 | + self.assertEqual(type(result), int) |
2040 | + |
2041 | + @defer.inlineCallbacks |
2042 | + def test_first_call_does_head(self): |
2043 | + """The first call gets the clock from our web.""" |
2044 | + checker = timestamp.TimestampChecker(self.wc) |
2045 | + yield checker.get_faithful_time() |
2046 | + self.assertEqual(self.ws.root.count, 1) |
2047 | + |
2048 | + @defer.inlineCallbacks |
2049 | + def test_second_call_is_cached(self): |
2050 | + """For the second call, the time is cached.""" |
2051 | + checker = timestamp.TimestampChecker(self.wc) |
2052 | + yield checker.get_faithful_time() |
2053 | + yield checker.get_faithful_time() |
2054 | + self.assertEqual(self.ws.root.count, 1) |
2055 | + |
2056 | + @defer.inlineCallbacks |
2057 | + def test_after_timeout_cache_expires(self): |
2058 | + """After some time, the cache expires.""" |
2059 | + fake_timestamp = 1 |
2060 | + self.patch(timestamp.time, "time", lambda: fake_timestamp) |
2061 | + checker = timestamp.TimestampChecker(self.wc) |
2062 | + yield checker.get_faithful_time() |
2063 | + fake_timestamp += timestamp.TimestampChecker.CHECKING_INTERVAL |
2064 | + yield checker.get_faithful_time() |
2065 | + self.assertEqual(self.ws.root.count, 2) |
2066 | + |
2067 | + @defer.inlineCallbacks |
2068 | + def test_server_error_means_skew_not_updated(self): |
2069 | + """When server can't be reached, the skew is not updated.""" |
2070 | + fake_timestamp = 1 |
2071 | + self.patch(timestamp.time, "time", lambda: fake_timestamp) |
2072 | + checker = timestamp.TimestampChecker(self.wc) |
2073 | + failing_get_server_time = lambda _: defer.fail(FakedError()) |
2074 | + self.patch(checker, "get_server_time", failing_get_server_time) |
2075 | + yield checker.get_faithful_time() |
2076 | + self.assertEqual(checker.skew, 0) |
2077 | + self.assertEqual(checker.next_check, |
2078 | + fake_timestamp + timestamp.TimestampChecker.ERROR_INTERVAL) |
2079 | + |
2080 | + @defer.inlineCallbacks |
2081 | + def test_server_date_sends_nocache_headers(self): |
2082 | + """Getting the server date sends the no-cache headers.""" |
2083 | + checker = timestamp.TimestampChecker(self.wc) |
2084 | + yield checker.get_server_date_header(self.ws.get_iri()) |
2085 | + self.assertEqual(len(self.ws.root.request_headers), 1) |
2086 | + headers = self.ws.root.request_headers[0] |
2087 | + result = headers.getRawHeaders("Cache-Control") |
2088 | + self.assertEqual(result, ["no-cache"]) |
2089 | |
2090 | === modified file 'ubuntu_sso/utils/webclient/tests/test_webclient.py' |
2091 | --- ubuntu_sso/utils/webclient/tests/test_webclient.py 2012-01-17 15:09:12 +0000 |
2092 | +++ ubuntu_sso/utils/webclient/tests/test_webclient.py 2012-01-31 21:52:24 +0000 |
2093 | @@ -28,6 +28,7 @@ |
2094 | from ubuntuone.devtools.testcases.squid import SquidTestCase |
2095 | |
2096 | from ubuntu_sso.utils import webclient |
2097 | +from ubuntu_sso.utils.webclient.common import HeaderDict |
2098 | |
2099 | ANY_VALUE = object() |
2100 | SAMPLE_KEY = "result" |
2101 | @@ -42,12 +43,15 @@ |
2102 | token_secret="the token secret", |
2103 | ) |
2104 | SAMPLE_HEADERS = {SAMPLE_KEY: SAMPLE_VALUE} |
2105 | +SAMPLE_POST_PARAMS = {"param1": "value1", "param2": "value2"} |
2106 | |
2107 | SIMPLERESOURCE = "simpleresource" |
2108 | +POSTABLERESOURECE = "postableresourece" |
2109 | THROWERROR = "throwerror" |
2110 | UNAUTHORIZED = "unauthorized" |
2111 | HEADONLY = "headonly" |
2112 | VERIFYHEADERS = "verifyheaders" |
2113 | +VERIFYPOSTPARAMS = "verifypostparams" |
2114 | GUARDED = "guarded" |
2115 | OAUTHRESOURCE = "oauthresource" |
2116 | |
2117 | @@ -70,6 +74,14 @@ |
2118 | return SAMPLE_RESOURCE |
2119 | |
2120 | |
2121 | +class PostableResource(resource.Resource): |
2122 | + """A resource that only answers to POST requests.""" |
2123 | + |
2124 | + def render_POST(self, request): |
2125 | + """Make a bit of html out of these resource's content.""" |
2126 | + return SAMPLE_RESOURCE |
2127 | + |
2128 | + |
2129 | class HeadOnlyResource(resource.Resource): |
2130 | """A resource that fails if called with a method other than HEAD.""" |
2131 | |
2132 | @@ -87,6 +99,29 @@ |
2133 | if headers != [SAMPLE_VALUE]: |
2134 | request.setResponseCode(http.BAD_REQUEST) |
2135 | return "ERROR: Expected header not present." |
2136 | + request.setHeader(SAMPLE_KEY, SAMPLE_VALUE) |
2137 | + return SAMPLE_RESOURCE |
2138 | + |
2139 | + |
2140 | +class VerifyPostParameters(resource.Resource): |
2141 | + """A resource that answers to POST requests with some parameters.""" |
2142 | + |
2143 | + def fetch_post_args_only(self, request): |
2144 | + """Fetch only the POST arguments, not the args in the url.""" |
2145 | + request.process = lambda: None |
2146 | + request.requestReceived(request.method, request.path, |
2147 | + request.clientproto) |
2148 | + return request.args |
2149 | + |
2150 | + def render_POST(self, request): |
2151 | + """Verify the parameters that we've been called with.""" |
2152 | + post_params = self.fetch_post_args_only(request) |
2153 | + expected = dict((key, [val]) for key, val |
2154 | + in SAMPLE_POST_PARAMS.items()) |
2155 | + if post_params != expected: |
2156 | + request.setResponseCode(http.BAD_REQUEST) |
2157 | + return "ERROR: Expected arguments not present, %r != %r" % ( |
2158 | + post_params, expected) |
2159 | return SAMPLE_RESOURCE |
2160 | |
2161 | |
2162 | @@ -142,6 +177,7 @@ |
2163 | """Start up this instance.""" |
2164 | root = resource.Resource() |
2165 | root.putChild(SIMPLERESOURCE, SimpleResource()) |
2166 | + root.putChild(POSTABLERESOURECE, PostableResource()) |
2167 | |
2168 | root.putChild(THROWERROR, resource.NoResource()) |
2169 | |
2170 | @@ -150,6 +186,7 @@ |
2171 | root.putChild(UNAUTHORIZED, unauthorized_resource) |
2172 | root.putChild(HEADONLY, HeadOnlyResource()) |
2173 | root.putChild(VERIFYHEADERS, VerifyHeadersResource()) |
2174 | + root.putChild(VERIFYPOSTPARAMS, VerifyPostParameters()) |
2175 | root.putChild(OAUTHRESOURCE, OAuthCheckerResource()) |
2176 | |
2177 | db = checkers.InMemoryUsernamePasswordDatabaseDontUse() |
2178 | @@ -259,6 +296,25 @@ |
2179 | webclient.WebClientError) |
2180 | |
2181 | @defer.inlineCallbacks |
2182 | + def test_post(self): |
2183 | + """Test a post request.""" |
2184 | + result = yield self.wc.request(self.base_iri + POSTABLERESOURECE, |
2185 | + method="POST") |
2186 | + self.assertEqual(SAMPLE_RESOURCE, result.content) |
2187 | + |
2188 | + @defer.inlineCallbacks |
2189 | + def test_post_with_args(self): |
2190 | + """Test a post request with arguments.""" |
2191 | + args = urllib.urlencode(SAMPLE_POST_PARAMS) |
2192 | + iri = self.base_iri + VERIFYPOSTPARAMS + "?" + args |
2193 | + headers = { |
2194 | + "content-type": "application/x-www-form-urlencoded", |
2195 | + } |
2196 | + result = yield self.wc.request(iri, method="POST", |
2197 | + extra_headers=headers, post_content=args) |
2198 | + self.assertEqual(SAMPLE_RESOURCE, result.content) |
2199 | + |
2200 | + @defer.inlineCallbacks |
2201 | def test_unauthorized(self): |
2202 | """Detect when a request failed with the UNAUTHORIZED http code.""" |
2203 | yield self.assertFailure(self.wc.request(self.base_iri + UNAUTHORIZED), |
2204 | @@ -275,7 +331,8 @@ |
2205 | """The extra_headers are sent to the server.""" |
2206 | result = yield self.wc.request(self.base_iri + VERIFYHEADERS, |
2207 | extra_headers=SAMPLE_HEADERS) |
2208 | - self.assertEqual(SAMPLE_RESOURCE, result.content) |
2209 | + self.assertIn(SAMPLE_KEY, result.headers) |
2210 | + self.assertEqual(result.headers[SAMPLE_KEY], [SAMPLE_VALUE]) |
2211 | |
2212 | @defer.inlineCallbacks |
2213 | def test_send_basic_auth(self): |
2214 | @@ -294,6 +351,22 @@ |
2215 | self.assertEqual(SAMPLE_RESOURCE, result.content) |
2216 | |
2217 | @defer.inlineCallbacks |
2218 | + def test_oauth_signing_uses_timestamp(self): |
2219 | + """OAuth signing uses the timestamp.""" |
2220 | + called = [] |
2221 | + |
2222 | + def fake_get_faithful_time(): |
2223 | + """A fake get_timestamp""" |
2224 | + called.append(True) |
2225 | + return defer.succeed(1) |
2226 | + |
2227 | + tsc = self.wc.get_timestamp_checker() |
2228 | + self.patch(tsc, "get_faithful_time", fake_get_faithful_time) |
2229 | + yield self.wc.request(self.base_iri + OAUTHRESOURCE, |
2230 | + oauth_credentials=SAMPLE_CREDENTIALS) |
2231 | + self.assertTrue(called, "The timestamp must be retrieved.") |
2232 | + |
2233 | + @defer.inlineCallbacks |
2234 | def test_returned_content_are_bytes(self): |
2235 | """The returned content are bytes.""" |
2236 | result = yield self.wc.request(self.base_iri + OAUTHRESOURCE, |
2237 | @@ -302,6 +375,38 @@ |
2238 | "The type of %r must be bytes" % result.content) |
2239 | |
2240 | |
2241 | +WebClientTestCase.skip = 'Tests failing, see bug #920591.' |
2242 | + |
2243 | + |
2244 | +class TimestampCheckerTestCase(TestCase): |
2245 | + """Tests for the timestampchecker classmethod.""" |
2246 | + |
2247 | + @defer.inlineCallbacks |
2248 | + def setUp(self): |
2249 | + """Initialize this testcase.""" |
2250 | + yield super(TimestampCheckerTestCase, self).setUp() |
2251 | + self.wc = webclient.webclient_factory() |
2252 | + self.patch(self.wc.__class__, "timestamp_checker", None) |
2253 | + |
2254 | + def test_timestamp_checker_has_the_same_class_as_the_creator(self): |
2255 | + """The TimestampChecker has the same class.""" |
2256 | + tsc = self.wc.get_timestamp_checker() |
2257 | + self.assertEqual(tsc.webclient.__class__, self.wc.__class__) |
2258 | + |
2259 | + def test_timestamp_checker_uses_different_webclient_than_the_creator(self): |
2260 | + """The TimestampChecker uses a different webclient than the creator.""" |
2261 | + tsc = self.wc.get_timestamp_checker() |
2262 | + self.assertNotEqual(tsc.webclient, self.wc) |
2263 | + |
2264 | + def test_timestamp_checker_is_the_same_for_all_webclients(self): |
2265 | + """The TimestampChecker is the same for all webclients.""" |
2266 | + tsc1 = self.wc.get_timestamp_checker() |
2267 | + wc2 = webclient.webclient_factory() |
2268 | + tsc2 = wc2.get_timestamp_checker() |
2269 | + # pylint: disable=E1101 |
2270 | + self.assertIs(tsc1, tsc2) |
2271 | + |
2272 | + |
2273 | class BasicProxyTestCase(SquidTestCase): |
2274 | """Test that the proxy works at all.""" |
2275 | |
2276 | @@ -340,6 +445,31 @@ |
2277 | test_authenticated_proxy_is_used.skip = reason |
2278 | |
2279 | |
2280 | +class HeaderDictTestCase(TestCase): |
2281 | + """Tests for the case insensitive header dictionary.""" |
2282 | + |
2283 | + def test_constructor_handles_keys(self): |
2284 | + """The constructor handles case-insensitive keys.""" |
2285 | + hd = HeaderDict({"ClAvE": "value"}) |
2286 | + self.assertIn("clave", hd) |
2287 | + |
2288 | + def test_can_set_get_items(self): |
2289 | + """The item is set/getted.""" |
2290 | + hd = HeaderDict() |
2291 | + hd["key"] = "value" |
2292 | + hd["KEY"] = "value2" |
2293 | + self.assertEqual(hd["key"], "value2") |
2294 | + |
2295 | + def test_can_test_presence(self): |
2296 | + """The presence of an item is found.""" |
2297 | + hd = HeaderDict() |
2298 | + self.assertNotIn("cLaVe", hd) |
2299 | + hd["CLAVE"] = "value1" |
2300 | + self.assertIn("cLaVe", hd) |
2301 | + del(hd["cLAVe"]) |
2302 | + self.assertNotIn("cLaVe", hd) |
2303 | + |
2304 | + |
2305 | class OAuthTestCase(TestCase): |
2306 | """Test for the oauth signing code.""" |
2307 | |
2308 | |
2309 | === added file 'ubuntu_sso/utils/webclient/timestamp.py' |
2310 | --- ubuntu_sso/utils/webclient/timestamp.py 1970-01-01 00:00:00 +0000 |
2311 | +++ ubuntu_sso/utils/webclient/timestamp.py 2012-01-31 21:52:24 +0000 |
2312 | @@ -0,0 +1,74 @@ |
2313 | +# -*- coding: utf-8 -*- |
2314 | +# |
2315 | +# Copyright 2011-2012 Canonical Ltd. |
2316 | +# |
2317 | +# This program is free software: you can redistribute it and/or modify it |
2318 | +# under the terms of the GNU General Public License version 3, as published |
2319 | +# by the Free Software Foundation. |
2320 | +# |
2321 | +# This program is distributed in the hope that it will be useful, but |
2322 | +# WITHOUT ANY WARRANTY; without even the implied warranties of |
2323 | +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR |
2324 | +# PURPOSE. See the GNU General Public License for more details. |
2325 | +# |
2326 | +# You should have received a copy of the GNU General Public License along |
2327 | +# with this program. If not, see <http://www.gnu.org/licenses/>. |
2328 | +"""Timestamp synchronization with the server.""" |
2329 | + |
2330 | +import time |
2331 | + |
2332 | +from twisted.internet import defer |
2333 | +from twisted.web import http |
2334 | + |
2335 | +from ubuntu_sso.logger import setup_logging |
2336 | + |
2337 | +logger = setup_logging("ubuntu_sso.utils.webclient.timestamp") |
2338 | +NOCACHE_HEADERS = {"Cache-Control": "no-cache"} |
2339 | + |
2340 | + |
2341 | +class TimestampChecker(object): |
2342 | + """A timestamp that's regularly checked with a server.""" |
2343 | + |
2344 | + CHECKING_INTERVAL = 60 * 60 # in seconds |
2345 | + ERROR_INTERVAL = 30 # in seconds |
2346 | + SERVER_IRI = u"http://one.ubuntu.com/api/time" |
2347 | + |
2348 | + def __init__(self, webclient): |
2349 | + """Initialize this instance.""" |
2350 | + self.next_check = time.time() |
2351 | + self.skew = 0 |
2352 | + self.webclient = webclient |
2353 | + |
2354 | + @defer.inlineCallbacks |
2355 | + def get_server_date_header(self, server_iri): |
2356 | + """Get the server date using twisted webclient.""" |
2357 | + response = yield self.webclient.request(server_iri, method="HEAD", |
2358 | + extra_headers=NOCACHE_HEADERS) |
2359 | + defer.returnValue(response.headers["Date"][0]) |
2360 | + |
2361 | + @defer.inlineCallbacks |
2362 | + def get_server_time(self): |
2363 | + """Get the time at the server.""" |
2364 | + date_string = yield self.get_server_date_header(self.SERVER_IRI) |
2365 | + timestamp = http.stringToDatetime(date_string) |
2366 | + defer.returnValue(timestamp) |
2367 | + |
2368 | + @defer.inlineCallbacks |
2369 | + def get_faithful_time(self): |
2370 | + """Get an accurate timestamp.""" |
2371 | + local_time = time.time() |
2372 | + if local_time >= self.next_check: |
2373 | + try: |
2374 | + server_time = yield self.get_server_time() |
2375 | + self.next_check = local_time + self.CHECKING_INTERVAL |
2376 | + self.skew = server_time - local_time |
2377 | + logger.debug("Calculated server-local time skew:", self.skew) |
2378 | + # We just log all exceptions while trying to get the server time |
2379 | + # pylint: disable=W0703 |
2380 | + except Exception, e: |
2381 | + logger.debug("Error while verifying the server time skew:", e) |
2382 | + self.next_check = local_time + self.ERROR_INTERVAL |
2383 | + logger.debug("Using corrected timestamp:", |
2384 | + http.datetimeToString(local_time + self.skew)) |
2385 | + defer.returnValue(int(local_time + self.skew)) |
2386 | +# timestamp_checker = TimestampChecker() |
2387 | |
2388 | === modified file 'ubuntu_sso/utils/webclient/txweb.py' |
2389 | --- ubuntu_sso/utils/webclient/txweb.py 2012-01-17 15:09:12 +0000 |
2390 | +++ ubuntu_sso/utils/webclient/txweb.py 2012-01-31 21:52:24 +0000 |
2391 | @@ -17,25 +17,70 @@ |
2392 | |
2393 | import base64 |
2394 | |
2395 | -from twisted.web import client, error, http |
2396 | -from twisted.internet import defer |
2397 | +from StringIO import StringIO |
2398 | + |
2399 | +from twisted.internet import defer, protocol, reactor |
2400 | +from twisted.web import client, http, http_headers, iweb |
2401 | +from zope.interface import implements |
2402 | |
2403 | from ubuntu_sso.utils.webclient.common import ( |
2404 | BaseWebClient, |
2405 | + HeaderDict, |
2406 | Response, |
2407 | UnauthorizedError, |
2408 | WebClientError, |
2409 | ) |
2410 | |
2411 | |
2412 | +class StringProtocol(protocol.Protocol): |
2413 | + """Hold the stuff received in a StringIO.""" |
2414 | + |
2415 | + # pylint: disable=C0103 |
2416 | + def __init__(self): |
2417 | + """Initialize this instance.""" |
2418 | + self.deferred = defer.Deferred() |
2419 | + self.content = StringIO() |
2420 | + |
2421 | + def dataReceived(self, data): |
2422 | + """Some more blocks received.""" |
2423 | + self.content.write(data) |
2424 | + |
2425 | + def connectionLost(self, reason=protocol.connectionDone): |
2426 | + """No more bytes available.""" |
2427 | + self.deferred.callback(self.content.getvalue()) |
2428 | + |
2429 | + |
2430 | +class StringProducer(object): |
2431 | + """Simple implementation of IBodyProducer.""" |
2432 | + implements(iweb.IBodyProducer) |
2433 | + |
2434 | + def __init__(self, body): |
2435 | + """Initialize this instance with some bytes.""" |
2436 | + self.body = body |
2437 | + self.length = len(body) |
2438 | + |
2439 | + # pylint: disable=C0103 |
2440 | + def startProducing(self, consumer): |
2441 | + """Start producing to the given IConsumer provider.""" |
2442 | + consumer.write(self.body) |
2443 | + return defer.succeed(None) |
2444 | + |
2445 | + def pauseProducing(self): |
2446 | + """In our case, do nothing.""" |
2447 | + |
2448 | + def stopProducing(self): |
2449 | + """In our case, do nothing.""" |
2450 | + |
2451 | + |
2452 | class WebClient(BaseWebClient): |
2453 | """A simple web client that does not support proxies, yet.""" |
2454 | |
2455 | @defer.inlineCallbacks |
2456 | def request(self, iri, method="GET", extra_headers=None, |
2457 | - oauth_credentials=None): |
2458 | + oauth_credentials=None, post_content=None): |
2459 | """Get the page, or fail trying.""" |
2460 | uri = self.iri_to_uri(iri) |
2461 | + |
2462 | if extra_headers: |
2463 | headers = dict(extra_headers) |
2464 | else: |
2465 | @@ -52,13 +97,39 @@ |
2466 | headers["Authorization"] = "Basic " + auth |
2467 | |
2468 | try: |
2469 | - result = yield client.getPage(uri, method=method, headers=headers) |
2470 | - response = Response(result) |
2471 | - defer.returnValue(response) |
2472 | - except error.Error as e: |
2473 | - if int(e.status) == http.UNAUTHORIZED: |
2474 | - raise UnauthorizedError(e.message) |
2475 | - raise WebClientError(e.message) |
2476 | + request_headers = http_headers.Headers() |
2477 | + for key, value in headers.items(): |
2478 | + request_headers.addRawHeader(key, value) |
2479 | + agent = client.Agent(reactor) |
2480 | + if post_content: |
2481 | + body_producer = StringProducer(post_content) |
2482 | + else: |
2483 | + body_producer = None |
2484 | + agent_response = yield agent.request(method, uri, |
2485 | + headers=request_headers, |
2486 | + bodyProducer=body_producer) |
2487 | + raw_headers = agent_response.headers.getAllRawHeaders() |
2488 | + response_headers = HeaderDict(raw_headers) |
2489 | + if method.lower() != "head": |
2490 | + response_content = yield self.get_agent_content(agent_response) |
2491 | + else: |
2492 | + response_content = "" |
2493 | + if agent_response.code == http.OK: |
2494 | + defer.returnValue(Response(response_content, response_headers)) |
2495 | + if agent_response.code == http.UNAUTHORIZED: |
2496 | + raise UnauthorizedError(agent_response.phrase, |
2497 | + response_content) |
2498 | + raise WebClientError(agent_response.phrase, response_content) |
2499 | + except WebClientError: |
2500 | + raise |
2501 | + except Exception as e: |
2502 | + raise WebClientError(e.message, e) |
2503 | + |
2504 | + def get_agent_content(self, agent_response): |
2505 | + """Get the contents of an agent response.""" |
2506 | + string_protocol = StringProtocol() |
2507 | + agent_response.deliverBody(string_protocol) |
2508 | + return string_protocol.deferred |
2509 | |
2510 | def force_use_proxy(self, settings): |
2511 | """Setup this webclient to use the given proxy settings.""" |
Branch looks good, tested IRL.