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

Proposed by Natalia Bidart
Status: Merged
Approved by: Natalia Bidart
Approved revision: 825
Merged at revision: 822
Proposed branch: lp:~nataliabidart/ubuntu-sso-client/stable-3-0-update-2.99.3
Merge into: lp:ubuntu-sso-client/stable-3-0
Diff against target: 2511 lines (+1165/-442)
21 files modified
bin/ubuntu-sso-login-gtk (+31/-0)
run-tests.bat (+120/-70)
setup.py (+11/-7)
ubuntu_sso/gtk/gui.py (+66/-76)
ubuntu_sso/gtk/main.py (+49/-0)
ubuntu_sso/gtk/tests/test_gui.py (+113/-191)
ubuntu_sso/gtk/tests/test_main.py (+123/-0)
ubuntu_sso/keyring/linux.py (+8/-17)
ubuntu_sso/main/linux.py (+2/-0)
ubuntu_sso/networkstate/linux.py (+0/-2)
ubuntu_sso/networkstate/tests/test_linux.py (+104/-20)
ubuntu_sso/utils/tests/test_txsecrets.py (+5/-0)
ubuntu_sso/utils/webclient/common.py (+43/-7)
ubuntu_sso/utils/webclient/libsoup.py (+6/-4)
ubuntu_sso/utils/webclient/qtnetwork.py (+7/-5)
ubuntu_sso/utils/webclient/restful.py (+13/-5)
ubuntu_sso/utils/webclient/tests/test_restful.py (+38/-27)
ubuntu_sso/utils/webclient/tests/test_timestamp.py (+140/-0)
ubuntu_sso/utils/webclient/tests/test_webclient.py (+131/-1)
ubuntu_sso/utils/webclient/timestamp.py (+74/-0)
ubuntu_sso/utils/webclient/txweb.py (+81/-10)
To merge this branch: bzr merge lp:~nataliabidart/ubuntu-sso-client/stable-3-0-update-2.99.3
Reviewer Review Type Date Requested Status
Alejandro J. Cura (community) Approve
Review via email: mp+90944@code.launchpad.net

Commit message

[ Natalia B. Bidart <email address hidden> ]
  - Skipping failing tests (see bug #920591).
  - Make webclient an installable module.
  - Make the GTK UI a separated executable script (LP: #917373).

[ Diego Sarmentero <email address hidden> ]
  - Delete signal removal to keep listening to network state changes (LP: #920591).

[ Alejandro J. Cura <email address hidden> ]
  - Restfulclient calls are now POST; response.headers are now case-insensitive
  dicts; OAuth timestamp sync with the server (LP: #916034).

[ Manuel de la Pena <email address hidden> ]
  - Add the possibility to skip the lint checks on windows when passing the
  /skip-lint parameter to run-tests.bat (LP: #918248).

To post a comment you must log in.
Revision history for this message
Alejandro J. Cura (alecu) wrote :

Branch looks good, tested IRL.

review: Approve
Revision history for this message
Ubuntu One Auto Pilot (otto-pilot) wrote :
Download full text (62.9 KiB)

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_sso.tests.test_account
  AccountTestCase
    test_generate_captcha ... [OK]
    test_is_not_validated ... [OK]
    test_is_not_validated_empty_result ... [OK]
    test_is_validated ... [OK]
    test_login_if_http_error ... [OK]
    test_login_if_no_error ... [OK]
    test_register_user_checks_valid_email ... [OK]
    test_register_user_checks_valid_password ... [OK]
    test_register_user_if_status_error ... [OK]
    test_register_user_if_status_error_with_string_message ... [OK]
    test_register_user_if_status_ok ... [OK]
    test_register_user_if_status_unknown ... [OK]
    test_request_password_reset_token_if_http_error ... [OK]
    test_request_password_reset_token_if_status_ok ... [OK]
    test_request_password_reset_token_if_status_unknown ... [OK]
    test_set_new_password_if_http_error ... [OK]
    test_set_new_password_if_status_ok ... [OK]
    test_set_new_password_if_status_unknown ... [OK]
    test_validate_email_if_status_error ... [OK]
    test_validate_email_if_status_error_with_string_message ... [OK]
    test_validate_email_if_status_ok ... [OK]
    test_validate_email_if_status_unknown ... [OK]
  EnvironOverridesTestCase
    test_no_override_service_url ... [OK]
    test_override_service_url ... [OK]
    test_service_url_as_parameter ... [OK]
twisted.trial.unittest
  TestCase
    runTest ... [OK]
ubuntu_sso.tests.test_account
  TimestampedAuthorizerTestCase
    test_authorize_request_includes_timestamp ... [OK]
ubuntu_sso.tests.test_credentials
  BasicTestCase
    runTest ... [OK]
  ClearCredentialsTestCase
    test_clear_credentials ... [OK]
    test_keyring_failure ... [OK]
  CredentialsAuthDeniedTestCase
    test_auth_denial_cb ... ...

825. By Natalia Bidart

- Fixed lint issues.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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."""

Subscribers

People subscribed via source and target branches