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 on 2012-01-31 | ||||||||||||||||
| 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) | 2012-01-31 | Approve on 2012-01-31 | |
|
Review via email:
|
|||
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 on 2012-01-31
-
- 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.