Merge lp:~dobey/ubuntu/oneiric/ubuntu-sso-client/release-133 into lp:ubuntu/oneiric/ubuntu-sso-client

Proposed by dobey on 2011-08-25
Status: Merged
Merged at revision: 36
Proposed branch: lp:~dobey/ubuntu/oneiric/ubuntu-sso-client/release-133
Merge into: lp:ubuntu/oneiric/ubuntu-sso-client
Diff against target: 4383 lines (+2124/-395)
39 files modified
PKG-INFO (+19/-1)
bin/windows-ubuntu-sso-login (+36/-5)
data/qt/current_user_sign_in.ui (+6/-3)
data/qt/email_verification.ui (+1/-1)
data/qt/forgotten_password.ui (+4/-1)
data/qt/reset_password.ui (+4/-1)
data/qt/setup_account.ui (+2/-15)
debian/changelog (+7/-0)
run-tests (+18/-2)
run-tests.bat (+5/-7)
setup.py (+33/-12)
ubuntu_sso/credentials.py (+46/-26)
ubuntu_sso/logger.py (+2/-3)
ubuntu_sso/main/__init__.py (+29/-7)
ubuntu_sso/main/linux.py (+20/-6)
ubuntu_sso/main/tests/test_common.py (+15/-8)
ubuntu_sso/main/tests/test_linux.py (+16/-5)
ubuntu_sso/main/tests/test_windows.py (+100/-15)
ubuntu_sso/main/windows.py (+87/-31)
ubuntu_sso/networkstate/windows.py (+4/-2)
ubuntu_sso/qt/controllers.py (+212/-78)
ubuntu_sso/qt/gui.py (+79/-16)
ubuntu_sso/qt/tests/login_u_p.py (+54/-0)
ubuntu_sso/qt/tests/show_gui.py (+16/-5)
ubuntu_sso/qt/tests/test_enchanced_line_edit.py (+94/-0)
ubuntu_sso/qt/tests/test_qt_views.py (+44/-1)
ubuntu_sso/qt/tests/test_windows.py (+502/-46)
ubuntu_sso/tests/__init__.py (+10/-10)
ubuntu_sso/tests/test_account.py (+5/-2)
ubuntu_sso/tests/test_credentials.py (+168/-82)
ubuntu_sso/utils/__init__.py (+35/-0)
ubuntu_sso/utils/tests/test_oauth_headers.py (+109/-0)
ubuntu_sso/utils/txsecrets.py (+8/-2)
ubuntu_sso/utils/ui.py (+3/-2)
ubuntu_sso/xdg_base_directory/__init__.py (+35/-0)
ubuntu_sso/xdg_base_directory/tests/__init__.py (+16/-0)
ubuntu_sso/xdg_base_directory/tests/test_common.py (+49/-0)
ubuntu_sso/xdg_base_directory/tests/test_windows.py (+112/-0)
ubuntu_sso/xdg_base_directory/windows.py (+119/-0)
To merge this branch: bzr merge lp:~dobey/ubuntu/oneiric/ubuntu-sso-client/release-133
Reviewer Review Type Date Requested Status
Ubuntu branches 2011-08-25 Pending
Review via email: mp+72943@code.launchpad.net
To post a comment you must log in.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'PKG-INFO'
2--- PKG-INFO 2011-07-22 20:12:20 +0000
3+++ PKG-INFO 2011-08-25 19:16:19 +0000
4@@ -1,6 +1,6 @@
5 Metadata-Version: 1.1
6 Name: ubuntu-sso-client
7-Version: 1.3.2
8+Version: 1.3.3
9 Summary: Ubuntu Single Sign-On client
10 Home-page: https://launchpad.net/ubuntu-sso-client
11 Author: Natalia Bidart
12@@ -8,4 +8,22 @@
13 License: GPL v3
14 Description: Desktop service to allow applications to sign into Ubuntu services via SSO
15 Platform: UNKNOWN
16+Requires: Image
17+Requires: PIL
18+Requires: PyQt4
19+Requires: dbus
20+Requires: gi.repository
21+Requires: gobject
22+Requires: gtk
23+Requires: keyring
24+Requires: mocker
25+Requires: oauth
26+Requires: twisted.internet
27+Requires: twisted.python
28+Requires: twisted.spread.pb
29+Requires: twisted.trial
30+Requires: ubuntuone.devtools.handlers
31+Requires: ubuntuone.devtools.testcase
32+Requires: webkit
33+Requires: xdg.BaseDirectory
34 Provides: ubuntu_sso
35
36=== modified file 'bin/windows-ubuntu-sso-login'
37--- bin/windows-ubuntu-sso-login 2011-07-22 20:12:20 +0000
38+++ bin/windows-ubuntu-sso-login 2011-08-25 19:16:19 +0000
39@@ -28,16 +28,25 @@
40 import qtreactor.qt4reactor
41 qtreactor.qt4reactor.install()
42
43-from twisted.internet import reactor
44+from twisted.internet import reactor, defer
45 from twisted.spread.pb import PBServerFactory
46 from twisted.internet.task import LoopingCall
47+from twisted.python import log
48
49+from ubuntu_sso.logger import setup_logging
50 from ubuntu_sso.main.windows import (
51 CredentialsManagement,
52+ LOCALHOST,
53 SSOCredentials,
54 SSOLogin,
55 UbuntuSSORoot,
56- get_sso_pb_hostport)
57+ get_activation_config,
58+)
59+from ubuntu_sso.utils import tcpactivation
60+
61+
62+logger = setup_logging("windows-ubuntu-sso-login")
63+
64
65 def add_timeout(interval, callback, *args, **kwargs):
66 """Add a timeout callback as a task."""
67@@ -45,13 +54,35 @@
68 time_out_task.start(interval/1000, now=False)
69
70
71-if __name__ == '__main__':
72- host, port = get_sso_pb_hostport()
73+@defer.inlineCallbacks
74+def main():
75+ """Initialize and start this process."""
76+ ai = tcpactivation.ActivationInstance(get_activation_config())
77+ port = yield ai.get_port()
78
79 login = SSOLogin('ignored')
80 creds = SSOCredentials()
81 creds_management = CredentialsManagement(add_timeout, reactor.stop)
82 root = UbuntuSSORoot(login, creds, creds_management)
83
84- listener = reactor.listenTCP(port, PBServerFactory(root), interface=host)
85+ reactor.listenTCP(port, PBServerFactory(root), interface=LOCALHOST)
86+
87+
88+def handle_already_started(failure):
89+ """Handle the already started error by shutting down this process."""
90+ failure.trap(tcpactivation.AlreadyStartedError)
91+ print "Ubuntu SSO login manager already running."
92+ reactor.stop()
93+
94+
95+def utter_failure(failure):
96+ """Handle an utter failure by logging it and quiting."""
97+ log.err(failure)
98+ reactor.stop()
99+
100+
101+if __name__ == '__main__':
102+ d = main()
103+ d.addErrback(handle_already_started)
104+ d.addErrback(utter_failure)
105 reactor.run()
106
107=== modified file 'data/qt/current_user_sign_in.ui'
108--- data/qt/current_user_sign_in.ui 2011-07-22 20:12:20 +0000
109+++ data/qt/current_user_sign_in.ui 2011-08-25 19:16:19 +0000
110@@ -6,14 +6,14 @@
111 <rect>
112 <x>0</x>
113 <y>0</y>
114- <width>400</width>
115- <height>300</height>
116+ <width>399</width>
117+ <height>309</height>
118 </rect>
119 </property>
120 <property name="windowTitle">
121 <string>WizardPage</string>
122 </property>
123- <layout class="QHBoxLayout" name="horizontalLayout">
124+ <layout class="QVBoxLayout" name="verticalLayout_4">
125 <item>
126 <layout class="QHBoxLayout" name="horizontalLayout_3">
127 <item>
128@@ -122,6 +122,9 @@
129 </item>
130 <item>
131 <widget class="QPushButton" name="sign_in_button">
132+ <property name="enabled">
133+ <bool>false</bool>
134+ </property>
135 <property name="text">
136 <string>Sign In</string>
137 </property>
138
139=== modified file 'data/qt/email_verification.ui'
140--- data/qt/email_verification.ui 2011-06-24 19:14:17 +0000
141+++ data/qt/email_verification.ui 2011-08-25 19:16:19 +0000
142@@ -13,7 +13,7 @@
143 <property name="windowTitle">
144 <string>WizardPage</string>
145 </property>
146- <layout class="QHBoxLayout" name="horizontalLayout">
147+ <layout class="QVBoxLayout" name="verticalLayout_2">
148 <item>
149 <layout class="QHBoxLayout" name="horizontalLayout_3">
150 <item>
151
152=== modified file 'data/qt/forgotten_password.ui'
153--- data/qt/forgotten_password.ui 2011-06-24 19:14:17 +0000
154+++ data/qt/forgotten_password.ui 2011-08-25 19:16:19 +0000
155@@ -7,7 +7,7 @@
156 <x>0</x>
157 <y>0</y>
158 <width>446</width>
159- <height>209</height>
160+ <height>317</height>
161 </rect>
162 </property>
163 <property name="windowTitle">
164@@ -109,6 +109,9 @@
165 </item>
166 <item>
167 <widget class="QPushButton" name="send_button">
168+ <property name="enabled">
169+ <bool>false</bool>
170+ </property>
171 <property name="text">
172 <string/>
173 </property>
174
175=== modified file 'data/qt/reset_password.ui'
176--- data/qt/reset_password.ui 2011-06-24 19:14:17 +0000
177+++ data/qt/reset_password.ui 2011-08-25 19:16:19 +0000
178@@ -7,7 +7,7 @@
179 <x>0</x>
180 <y>0</y>
181 <width>476</width>
182- <height>282</height>
183+ <height>262</height>
184 </rect>
185 </property>
186 <property name="windowTitle">
187@@ -65,6 +65,9 @@
188 </item>
189 <item>
190 <widget class="QPushButton" name="reset_password_button">
191+ <property name="enabled">
192+ <bool>false</bool>
193+ </property>
194 <property name="text">
195 <string/>
196 </property>
197
198=== modified file 'data/qt/setup_account.ui'
199--- data/qt/setup_account.ui 2011-06-24 19:14:17 +0000
200+++ data/qt/setup_account.ui 2011-08-25 19:16:19 +0000
201@@ -7,29 +7,16 @@
202 <x>0</x>
203 <y>0</y>
204 <width>407</width>
205- <height>453</height>
206+ <height>572</height>
207 </rect>
208 </property>
209 <property name="windowTitle">
210 <string>WizardPage</string>
211 </property>
212- <layout class="QHBoxLayout" name="horizontalLayout">
213+ <layout class="QVBoxLayout" name="verticalLayout_5">
214 <item>
215 <layout class="QVBoxLayout" name="verticalLayout">
216 <item>
217- <spacer name="verticalSpacer_3">
218- <property name="orientation">
219- <enum>Qt::Vertical</enum>
220- </property>
221- <property name="sizeHint" stdset="0">
222- <size>
223- <width>20</width>
224- <height>40</height>
225- </size>
226- </property>
227- </spacer>
228- </item>
229- <item>
230 <widget class="QFrame" name="_signInFrame">
231 <property name="frameShape">
232 <enum>QFrame::NoFrame</enum>
233
234=== modified file 'debian/changelog'
235--- debian/changelog 2011-07-26 13:58:21 +0000
236+++ debian/changelog 2011-08-25 19:16:19 +0000
237@@ -1,3 +1,10 @@
238+ubuntu-sso-client (1.3.3-0ubuntu1) oneiric; urgency=low
239+
240+ * New upstream release.
241+ - Work correctly with static and GI bindings of gobject (LP: #829186)
242+
243+ -- Rodney Dawes <rodney.dawes@ubuntu.com> Thu, 25 Aug 2011 15:08:27 -0400
244+
245 ubuntu-sso-client (1.3.2-0ubuntu2) oneiric; urgency=low
246
247 * Require python-qt4 to build, as distutils doesn't have optional deps
248
249=== modified file 'run-tests'
250--- run-tests 2011-07-22 20:12:20 +0000
251+++ run-tests 2011-08-25 19:16:19 +0000
252@@ -16,8 +16,18 @@
253 # You should have received a copy of the GNU General Public License along
254 # with this program. If not, see <http://www.gnu.org/licenses/>.
255
256+QT_TESTS_PATH=ubuntu_sso/qt/tests
257+GTK_TESTS_PATH=ubuntu_sso/gtk/tests
258+
259 set -e
260
261+if [ "$1" == "-qt" ]; then
262+ USE_QT=1
263+ shift
264+else
265+ USE_QT=0
266+fi
267+
268 if [ $# -ne 0 ]; then
269 # run specific module given by the caller
270 MODULE="$@"
271@@ -30,7 +40,7 @@
272 ./setup.py clean
273 u1lint
274 if [ -x `which pep8` ]; then
275- pep8 --repeat bin/ $MODULE
276+ pep8 --repeat .
277 else
278 echo "Please install the 'pep8' package."
279 fi
280@@ -39,6 +49,12 @@
281 unset GTK_MODULES
282
283 echo "Running test suite for ""$MODULE"
284-`which xvfb-run` u1trial "$MODULE" -i "test_windows.py, test_qt_views.py"
285+if [ "$USE_QT" -eq 0 ]; then
286+ `which xvfb-run` u1trial -p "$QT_TESTS_PATH" -i "test_windows.py" "$MODULE"
287+else
288+ ./setup.py build
289+ `which xvfb-run` u1trial -p "$GTK_TESTS_PATH" -i "test_windows.py" --reactor=qt4 --gui "$MODULE"
290+fi
291 style_check
292 rm -rf _trial_temp
293+rm -rf build
294
295=== modified file 'run-tests.bat'
296--- run-tests.bat 2011-07-22 20:12:20 +0000
297+++ run-tests.bat 2011-08-25 19:16:19 +0000
298@@ -60,13 +60,11 @@
299 ECHO Cleaning the generated code before running the style checks...
300 "%PYTHONEXEPATH%\python.exe" setup.py clean
301 ECHO Performing style checks...
302-"%PYTHONEXEPATH%\python.exe" "%PYTHONEXEPATH%\Scripts\u1lint" ubuntu_sso
303+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"
304+"%PYTHONEXEPATH%\python.exe" "%PYTHONEXEPATH%\Scripts\u1lint" -i "%IGNORE_LINT%" ubuntu_sso
305 :: test for style if we can, if pep8 is not present, move to the end
306-IF EXIST "%PYTHONEXEPATH%\Scripts\pep8.exe"
307-"%PYTHONEXEPATH%\Scripts\pep8.exe" --repeat ubuntu_sso
308-ELSE
309-ECHO Style checks were not done
310+"%PYTHONEXEPATH%\Scripts\pep8.exe" --repeat .
311 :: Delete the temp folders
312-RMDIR /s /q _trial_temp
313-RMDIR /s /q .coverage
314+RMDIR /q /s _trial_temp
315+RMDIR /q /s .coverage
316 :END
317
318=== modified file 'setup.py'
319--- setup.py 2011-07-22 20:12:20 +0000
320+++ setup.py 2011-08-25 19:16:19 +0000
321@@ -22,6 +22,7 @@
322
323 # pylint: disable=W0404, W0511
324
325+import distutils
326 import os
327 import sys
328
329@@ -43,6 +44,7 @@
330 CLEANFILES = [SERVICE_FILE, POT_FILE, 'MANIFEST']
331 QT_UI_DIR = os.path.join('ubuntu_sso', 'qt')
332
333+
334 def replace_prefix(prefix):
335 """Replace every '@prefix@' with prefix within 'filename' content."""
336 # replace .service file
337@@ -242,7 +244,7 @@
338 # this some day. If this doesn't work, try import modulefinder
339 for package_path in win32com.__path__[1:]:
340 modulefinder.AddPackagePath("win32com", package_path)
341- for extra_mod in ["win32com.server" ,"win32com.client"]:
342+ for extra_mod in ["win32com.server", "win32com.client"]:
343 __import__(extra_mod)
344 module = sys.modules[extra_mod]
345 for module_path in module.__path__[1:]:
346@@ -251,7 +253,7 @@
347 # lazr uses namespaces packages, which does add some problems to py2exe
348 # the following is a way to work arround the issue
349 for path in lazr.__path__:
350- modulefinder.AddPackagePath(__name__, path)
351+ modulefinder.AddPackagePath(__name__, path)
352
353
354 def get_py2exe_extension():
355@@ -261,14 +263,10 @@
356 from py2exe.build_exe import py2exe as build_exe
357 # pylint: enable=F0401
358
359- # pylint: disable=E1101
360+ # pylint: disable=E1101,W0232
361 class MediaCollector(build_exe):
362 """Extension that copies lazr missing data."""
363
364- def __init__(self, *args, **kwargs):
365- """Create a new instance."""
366- build_exe.__init__(self, *args, **kwargs)
367-
368 def _add_module_data(self, module_name):
369 """Add the data from a given path."""
370 # Create the media subdir where the
371@@ -310,6 +308,24 @@
372 'clean': SSOClean,
373 }
374
375+
376+class dummy_build_i18n(distutils.cmd.Command):
377+
378+ """Dummy for windows."""
379+
380+ def initialize_options(self, *args):
381+ """Dummy."""
382+
383+ def finalize_options(self, *args):
384+ """Dummy."""
385+
386+ def run(self, *args):
387+ """Dummy."""
388+
389+if sys.platform == 'win32':
390+ cmdclass['build_i18n'] = dummy_build_i18n
391+
392+
393 def setup_windows():
394 """Provide the required info to setup the project on windows."""
395 set_py2exe_paths()
396@@ -329,7 +345,7 @@
397 },
398 },
399 # add the console script so that py2exe compiles it
400- 'console': ['bin/windows-ubuntu-sso-login',],
401+ 'console': ['bin/windows-ubuntu-sso-login'],
402 'zipfile': None,
403 }
404 return _data_files, _extra
405@@ -344,7 +360,7 @@
406
407 DistUtilsExtra.auto.setup(
408 name='ubuntu-sso-client',
409- version='1.3.2',
410+ version='1.3.3',
411 license='GPL v3',
412 author='Natalia Bidart',
413 author_email='natalia.bidart@canonical.com',
414@@ -355,9 +371,14 @@
415 extra_path='ubuntu-sso-client',
416 data_files=data_files,
417 packages=[
418- 'ubuntu_sso', 'ubuntu_sso.utils', 'ubuntu_sso.keyring',
419- 'ubuntu_sso.networkstate', 'ubuntu_sso.main',
420- 'ubuntu_sso.gtk', 'ubuntu_sso.qt',
421+ 'ubuntu_sso',
422+ 'ubuntu_sso.utils',
423+ 'ubuntu_sso.keyring',
424+ 'ubuntu_sso.networkstate',
425+ 'ubuntu_sso.main',
426+ 'ubuntu_sso.gtk',
427+ 'ubuntu_sso.qt',
428+ 'ubuntu_sso.xdg_base_directory',
429 ],
430 cmdclass=cmdclass,
431 **extra)
432
433=== modified file 'ubuntu_sso/credentials.py'
434--- ubuntu_sso/credentials.py 2011-01-12 18:56:56 +0000
435+++ ubuntu_sso/credentials.py 2011-08-25 19:16:19 +0000
436@@ -43,14 +43,12 @@
437
438 from functools import wraps
439
440-from oauth import oauth
441 from twisted.internet.defer import inlineCallbacks, returnValue
442
443-from ubuntu_sso import NO_OP
444+from ubuntu_sso import NO_OP, utils
445 from ubuntu_sso.keyring import Keyring
446 from ubuntu_sso.logger import setup_logging
447
448-
449 logger = setup_logging('ubuntu_sso.credentials')
450
451
452@@ -183,7 +181,7 @@
453 self._success_cb = success_cb
454 self._error_cb = error_cb
455 self.denial_cb = denial_cb
456- self.gui = None # will hold the GUI instance
457+ self.inner = None # will hold the GUI or SSOLoginRoot instance
458
459 @handle_failures(msg='Problem while retrieving credentials')
460 @inlineCallbacks
461@@ -226,27 +224,28 @@
462 defined if this method is being called.
463
464 """
465- logger.info('Pinging server for app_name "%s", ping_url: "%s", '
466- 'email "%s".', app_name, self.ping_url, email)
467- url = self.ping_url + email
468- consumer = oauth.OAuthConsumer(credentials['consumer_key'],
469- credentials['consumer_secret'])
470- token = oauth.OAuthToken(credentials['token'],
471- credentials['token_secret'])
472- get_request = oauth.OAuthRequest.from_consumer_and_token
473- oauth_req = get_request(oauth_consumer=consumer, token=token,
474- http_method='GET', http_url=url)
475- oauth_req.sign_request(oauth.OAuthSignatureMethod_HMAC_SHA1(),
476- consumer, token)
477- request = urllib2.Request(url, headers=oauth_req.to_header())
478- logger.debug('Opening the url "%s" with urllib2.urlopen.',
479+ logger.info('Pinging server for app_name %r, ping_url: %r, '
480+ 'email %r.', app_name, self.ping_url, email)
481+ try:
482+ url = self.ping_url.format(email=email)
483+ except IndexError: # tuple index out of range
484+ url = self.ping_url.format(email) # format the first substitution
485+
486+ if url == self.ping_url:
487+ logger.debug('Original url (%r) could not be formatted, '
488+ 'appending email (%r).', self.ping_url, email)
489+ url = self.ping_url + email
490+
491+ headers = utils.oauth_headers(url, credentials)
492+ request = urllib2.Request(url, headers=headers)
493+ logger.debug('Opening the url %r with urllib2.urlopen.',
494 request.get_full_url())
495 # This code is blocking, we should change this.
496 # I've tried with deferToThread an twisted.web.client.getPage
497 # but the returned deferred will never be fired (nataliabidart).
498 response = urllib2.urlopen(request)
499 logger.debug('Url opened. Response: %s.', response.code)
500- returnValue(response.code)
501+ returnValue(response)
502
503 @handle_exceptions(msg='Problem opening the Ubuntu SSO user interface')
504 def _show_ui(self, login_only):
505@@ -255,23 +254,39 @@
506 __import__(self.ui_module)
507 gui = sys.modules[self.ui_module]
508
509- self.gui = getattr(gui, self.ui_class)(app_name=self.app_name,
510+ self.inner = getattr(gui, self.ui_class)(app_name=self.app_name,
511 tc_url=self.tc_url, help_text=self.help_text,
512 window_id=self.window_id, login_only=login_only)
513
514- self.gui.login_success_callback = self._login_success_cb
515- self.gui.registration_success_callback = self._login_success_cb
516- self.gui.user_cancellation_callback = self._auth_denial_cb
517+ self.inner.login_success_callback = self._login_success_cb
518+ self.inner.registration_success_callback = self._login_success_cb
519+ self.inner.user_cancellation_callback = self._auth_denial_cb
520+
521+ @handle_exceptions(msg='Problem logging with email and password.')
522+ def _do_login(self, email, password):
523+ """Login using email/password, connect outcome signals."""
524+ from ubuntu_sso.main import SSOLoginRoot
525+ self.inner = SSOLoginRoot()
526+ self.inner.login(app_name=self.app_name, email=email,
527+ password=password,
528+ result_cb=self._login_success_cb,
529+ error_cb=self._error_cb,
530+ not_validated_cb=self._error_cb)
531
532 @handle_failures(msg='Problem while retrieving credentials')
533 @inlineCallbacks
534- def _login_or_register(self, login_only):
535+ def _login_or_register(self, login_only, email=None, password=None):
536 """Get credentials if found else prompt the GUI."""
537+ logger.info("_login_or_register: login_only=%r email=%r.",
538+ login_only, email)
539 token = yield self.find_credentials()
540 if token is not None and len(token) > 0:
541 self.success_cb(token)
542 elif token == {}:
543- self._show_ui(login_only)
544+ if email and password:
545+ self._do_login(email, password)
546+ else:
547+ self._show_ui(login_only)
548 else:
549 # something went wrong with find_credentials, already handled.
550 logger.info('_login_or_register: call to "find_credentials" went '
551@@ -292,7 +307,7 @@
552 def find_credentials(self):
553 """Get the credentials for 'self.app_name'. Return {} if not there."""
554 creds = yield Keyring().get_credentials(self.app_name)
555- logger.info('find_credentials: self.app_name "%s", '
556+ logger.info('find_credentials: self.app_name %r, '
557 'result is {}? %s', self.app_name, creds is None)
558 returnValue(creds if creds is not None else {})
559
560@@ -313,3 +328,8 @@
561 def login(self):
562 """Get credentials if found else prompt the GUI to login."""
563 return self._login_or_register(login_only=True)
564+
565+ def login_email_password(self, email, password):
566+ """Get credentials if found else login using email and password."""
567+ return self._login_or_register(login_only=True,
568+ email=email, password=password)
569
570=== modified file 'ubuntu_sso/logger.py'
571--- ubuntu_sso/logger.py 2011-01-12 18:56:56 +0000
572+++ ubuntu_sso/logger.py 2011-08-25 19:16:19 +0000
573@@ -25,12 +25,11 @@
574 import os
575 import sys
576
577-import xdg.BaseDirectory
578-
579 from logging.handlers import RotatingFileHandler
580
581+from ubuntu_sso import xdg_base_directory
582
583-LOGFOLDER = os.path.join(xdg.BaseDirectory.xdg_cache_home, 'sso')
584+LOGFOLDER = os.path.join(xdg_base_directory.xdg_cache_home, 'sso')
585 # create log folder if it doesn't exists
586 if not os.path.exists(LOGFOLDER):
587 os.makedirs(LOGFOLDER)
588
589=== modified file 'ubuntu_sso/main/__init__.py'
590--- ubuntu_sso/main/__init__.py 2011-07-22 20:12:20 +0000
591+++ ubuntu_sso/main/__init__.py 2011-08-25 19:16:19 +0000
592@@ -75,7 +75,7 @@
593 self.processor = self.sso_login_processor_class(
594 sso_service_class=sso_service_class)
595
596- def generate_captcha(self, app_name, filename, thread_execute, result_cb,
597+ def generate_captcha(self, app_name, filename, result_cb,
598 error_cb):
599 """Call the matching method in the processor."""
600 def f():
601@@ -84,7 +84,7 @@
602 thread_execute(f, app_name, result_cb, error_cb)
603
604 def register_user(self, app_name, email, password, name, captcha_id,
605- captcha_solution, thread_execute, result_cb, error_cb):
606+ captcha_solution, result_cb, error_cb):
607 """Call the matching method in the processor."""
608 def f():
609 """Inner function that will be run in a thread."""
610@@ -92,7 +92,7 @@
611 captcha_id, captcha_solution)
612 thread_execute(f, app_name, result_cb, error_cb)
613
614- def login(self, app_name, email, password, thread_execute, result_cb,
615+ def login(self, app_name, email, password, result_cb,
616 error_cb, not_validated_cb):
617 """Call the matching method in the processor."""
618 def f():
619@@ -121,7 +121,7 @@
620 thread_execute(f, app_name, success_cb, error_cb)
621
622 def validate_email(self, app_name, email, password, email_token,
623- thread_execute, result_cb, error_cb):
624+ result_cb, error_cb):
625 """Call the matching method in the processor."""
626
627 def f():
628@@ -141,7 +141,7 @@
629
630 thread_execute(f, app_name, success_cb, error_cb)
631
632- def request_password_reset_token(self, app_name, email, thread_execute,
633+ def request_password_reset_token(self, app_name, email,
634 result_cb, error_cb):
635 """Call the matching method in the processor."""
636 def f():
637@@ -150,7 +150,7 @@
638 thread_execute(f, app_name, result_cb, error_cb)
639
640 def set_new_password(self, app_name, email, token, new_password,
641- thread_execute, result_cb, error_cb):
642+ result_cb, error_cb):
643 """Call the matching method in the processor."""
644 def f():
645 """Inner function that will be run in a thread."""
646@@ -160,10 +160,16 @@
647
648
649 class SSOCredentialsRoot(object):
650- """Object that gets credentials, and login/registers if needed."""
651+ """Object that gets credentials, and login/registers if needed.
652+
653+ This class is DEPRECATED, use CredentialsManagementRoot instead.
654+
655+ """
656
657 def __init__(self):
658 self.ping_url = os.environ.get('USSOC_PING_URL', U1_PING_URL)
659+ msg = 'Use ubuntu_sso.main.CredentialsManagementRoot instead.'
660+ warnings.warn(msg, DeprecationWarning)
661
662 def find_credentials(self, app_name, callback=NO_OP, errback=NO_OP):
663 """Get the credentials from the keyring or {} if not there."""
664@@ -399,6 +405,20 @@
665 obj = Credentials(app_name, **self._parse_args(args))
666 obj.login()
667
668+ def login_email_password(self, app_name, args):
669+ """Get credentials if found else try to login.
670+
671+ Login will be done by inspecting 'args' and expecting to find two keys:
672+ 'email' and 'password'.
673+
674+ """
675+ self.ref_count += 1
676+ email = args.pop('email')
677+ password = args.pop('password')
678+ obj = Credentials(app_name, **self._parse_args(args))
679+ obj.login_email_password(email=email, password=password)
680+
681+
682 # pylint: disable=C0103
683 SSOLogin = None
684 SSOCredentials = None
685@@ -410,8 +430,10 @@
686 SSOCredentials = windows.SSOCredentials
687 CredentialsManagement = windows.CredentialsManagement
688 TIMEOUT_INTERVAL = 10000000000 # forever
689+ thread_execute = windows.blocking
690 else:
691 from ubuntu_sso.main import linux
692 SSOLogin = linux.SSOLogin
693 SSOCredentials = linux.SSOCredentials
694 CredentialsManagement = linux.CredentialsManagement
695+ thread_execute = linux.blocking
696
697=== modified file 'ubuntu_sso/main/linux.py'
698--- ubuntu_sso/main/linux.py 2011-06-24 19:14:17 +0000
699+++ ubuntu_sso/main/linux.py 2011-08-25 19:16:19 +0000
700@@ -74,6 +74,8 @@
701 dbus.service.Object.__init__(self, object_path=object_path,
702 bus_name=bus_name)
703 self.root = SSOLoginRoot(sso_login_processor_class, sso_service_class)
704+ msg = 'Use ubuntu_sso.main.CredentialsManagement instead.'
705+ warnings.warn(msg, DeprecationWarning)
706
707 # generate_capcha signals
708 @dbus.service.signal(DBUS_IFACE_USER_NAME, signature="ss")
709@@ -92,7 +94,7 @@
710 in_signature='ss')
711 def generate_captcha(self, app_name, filename):
712 """Call the matching method in the processor."""
713- self.root.generate_captcha(app_name, filename, blocking,
714+ self.root.generate_captcha(app_name, filename,
715 self.CaptchaGenerated,
716 self.CaptchaGenerationError)
717
718@@ -115,7 +117,7 @@
719 captcha_id, captcha_solution):
720 """Call the matching method in the processor."""
721 self.root.register_user(app_name, email, password, name, captcha_id,
722- captcha_solution, blocking,
723+ captcha_solution,
724 self.UserRegistered,
725 self.UserRegistrationError)
726
727@@ -142,7 +144,7 @@
728 in_signature='sss')
729 def login(self, app_name, email, password):
730 """Call the matching method in the processor."""
731- self.root.login(app_name, email, password, blocking, self.LoggedIn,
732+ self.root.login(app_name, email, password, self.LoggedIn,
733 self.LoginError, self.UserNotValidated)
734
735 # validate_email signals
736@@ -163,7 +165,7 @@
737 def validate_email(self, app_name, email, password, email_token):
738 """Call the matching method in the processor."""
739 self.root.validate_email(app_name, email, password, email_token,
740- blocking, self.EmailValidated,
741+ self.EmailValidated,
742 self.EmailValidationError)
743
744 # request_password_reset_token signals
745@@ -183,7 +185,7 @@
746 in_signature='ss')
747 def request_password_reset_token(self, app_name, email):
748 """Call the matching method in the processor."""
749- self.root.request_password_reset_token(app_name, email, blocking,
750+ self.root.request_password_reset_token(app_name, email,
751 self.PasswordResetTokenSent,
752 self.PasswordResetError)
753
754@@ -205,7 +207,7 @@
755 def set_new_password(self, app_name, email, token, new_password):
756 """Call the matching method in the processor."""
757 self.root.set_new_password(app_name, email, token, new_password,
758- blocking, self.PasswordChanged,
759+ self.PasswordChanged,
760 self.PasswordChangeError)
761
762
763@@ -462,3 +464,15 @@
764 def login(self, app_name, args):
765 """Get credentials if found else prompt GUI to login."""
766 self.root.login(app_name, args)
767+
768+ @dbus.service.method(dbus_interface=DBUS_CREDENTIALS_IFACE,
769+ in_signature='sa{ss}', out_signature='')
770+ def login_email_password(self, app_name, args):
771+ """Get credentials if found, else login using email and password.
772+
773+ - 'args' should contain at least the follwing keys: 'email' and
774+ 'password'. Those will be used to issue a new SSO token, which will be
775+ returned trough the CredentialsFound signal.
776+
777+ """
778+ self.root.login_email_password(app_name, args)
779
780=== modified file 'ubuntu_sso/main/tests/test_common.py'
781--- ubuntu_sso/main/tests/test_common.py 2011-06-24 19:14:17 +0000
782+++ ubuntu_sso/main/tests/test_common.py 2011-08-25 19:16:19 +0000
783@@ -75,7 +75,7 @@
784 """Test that the call is relayed."""
785 app_name = 'app'
786 filename = 'file'
787- self.root.generate_captcha(app_name, filename, MATCH(callable),
788+ self.root.generate_captcha(app_name, filename,
789 MATCH(callable), MATCH(callable))
790 self.mocker.replay()
791 self.login.generate_captcha(app_name, filename)
792@@ -89,7 +89,7 @@
793 captcha_id = 'id'
794 captcha_solution = 'hello'
795 self.root.register_user(app_name, email, password, name, captcha_id,
796- captcha_solution, MATCH(callable),
797+ captcha_solution,
798 MATCH(callable), MATCH(callable))
799 self.mocker.replay()
800 self.login.register_user(app_name, email, password, name, captcha_id,
801@@ -100,7 +100,7 @@
802 app_name = 'app'
803 email = 'email'
804 password = 'password'
805- self.root.login(app_name, email, password, MATCH(callable),
806+ self.root.login(app_name, email, password,
807 MATCH(callable), MATCH(callable),
808 MATCH(callable))
809 self.mocker.mock()
810@@ -114,18 +114,17 @@
811 password = 'passwrd'
812 email_token = 'token'
813 self.root.validate_email(app_name, email, password, email_token,
814- MATCH(callable), MATCH(callable),
815+ MATCH(callable),
816 MATCH(callable))
817 self.mocker.replay()
818 self.login.validate_email(app_name, email, password, email_token)
819
820- def test_request_password_reset_tolen(self):
821+ def test_request_password_reset_token(self):
822 """Test that the call is relayed."""
823 app_name = 'app'
824 email = 'email'
825 self.root.request_password_reset_token(app_name, email,
826 MATCH(callable),
827- MATCH(callable),
828 MATCH(callable))
829 self.mocker.replay()
830 self.login.request_password_reset_token(app_name, email)
831@@ -137,7 +136,7 @@
832 token = 'token'
833 new_password = 'new'
834 self.root.set_new_password(app_name, email, token, new_password,
835- MATCH(callable), MATCH(callable),
836+ MATCH(callable),
837 MATCH(callable))
838 self.mocker.replay()
839 self.login.set_new_password(app_name, email, token, new_password)
840@@ -190,7 +189,7 @@
841 self.cred.clear_token(app_name, result_cb, error_cb)
842
843
844-class CredentialsManagementMockedTestCase(MockerTestCase):
845+class CredentialsManagementMockedTestCase(MockerTestCase, TestCase):
846 """Test that the call are relied correctly."""
847
848 def setUp(self):
849@@ -242,3 +241,11 @@
850 self.root.login(app_name, args)
851 self.mocker.replay()
852 self.cred.login(app_name, args)
853+
854+ def test_login_email_password(self):
855+ """Test that the call is relayed."""
856+ app_name = 'app'
857+ args = 'args'
858+ self.root.login_email_password(app_name, args)
859+ self.mocker.replay()
860+ self.cred.login_email_password(app_name, args)
861
862=== modified file 'ubuntu_sso/main/tests/test_linux.py'
863--- ubuntu_sso/main/tests/test_linux.py 2011-06-24 19:14:17 +0000
864+++ ubuntu_sso/main/tests/test_linux.py 2011-08-25 19:16:19 +0000
865@@ -144,7 +144,7 @@
866 expected_result = "expected result"
867 self.create_mock_processor().generate_captcha(filename)
868 self.mocker.result(expected_result)
869- self.patch(ubuntu_sso.main.linux, "blocking", fake_err_blocking)
870+ self.patch(ubuntu_sso.main, "thread_execute", fake_err_blocking)
871 self.mocker.replay()
872
873 def verify(app_name, errdict):
874@@ -191,7 +191,7 @@
875 self.create_mock_processor().register_user(EMAIL, PASSWORD, NAME,
876 CAPTCHA_ID, CAPTCHA_SOLUTION)
877 self.mocker.result(expected_result)
878- self.patch(ubuntu_sso.main.linux, "blocking", fake_err_blocking)
879+ self.patch(ubuntu_sso.main, "thread_execute", fake_err_blocking)
880 self.mocker.replay()
881
882 def verify(app_name, errdict):
883@@ -377,7 +377,7 @@
884 d = Deferred()
885 processor = self.create_mock_processor()
886 processor.request_password_reset_token(EMAIL)
887- self.patch(ubuntu_sso.main.linux, "blocking", fake_ok_blocking)
888+ self.patch(ubuntu_sso.main, "thread_execute", fake_ok_blocking)
889 self.mocker.result(EMAIL)
890 self.mocker.replay()
891
892@@ -400,7 +400,7 @@
893
894 self.create_mock_processor().request_password_reset_token(EMAIL)
895 self.mocker.result(EMAIL)
896- self.patch(ubuntu_sso.main.linux, "blocking", fake_err_blocking)
897+ self.patch(ubuntu_sso.main, "thread_execute", fake_err_blocking)
898 self.mocker.replay()
899
900 def verify(app_name, errdict):
901@@ -446,7 +446,7 @@
902 self.create_mock_processor().set_new_password(EMAIL, EMAIL_TOKEN,
903 PASSWORD)
904 self.mocker.result(expected_result)
905- self.patch(ubuntu_sso.main.linux, "blocking", fake_err_blocking)
906+ self.patch(ubuntu_sso.main, "thread_execute", fake_err_blocking)
907 self.mocker.replay()
908
909 def verify(app_name, errdict):
910@@ -1222,6 +1222,17 @@
911 self.client.login(APP_NAME, self.args)
912 self.assert_dbus_method_correct(self.client.login)
913
914+ def test_login_email_password(self):
915+ """The login_email_password is correct."""
916+ self.create_mock_backend().login_email_password(email=EMAIL,
917+ password=PASSWORD)
918+ self.mocker.replay()
919+
920+ self.args['email'] = EMAIL
921+ self.args['password'] = PASSWORD
922+ self.client.login_email_password(APP_NAME, self.args)
923+ self.assert_dbus_method_correct(self.client.login_email_password)
924+
925
926 class CredentialsManagementParamsTestCase(CredentialsManagementOpsTestCase):
927 """Tests for the CredentialsManagement extra parameters handling."""
928
929=== modified file 'ubuntu_sso/main/tests/test_windows.py'
930--- ubuntu_sso/main/tests/test_windows.py 2011-07-22 20:12:20 +0000
931+++ ubuntu_sso/main/tests/test_windows.py 2011-08-25 19:16:19 +0000
932@@ -30,10 +30,10 @@
933 )
934 from ubuntu_sso.main import windows
935 from ubuntu_sso.main.windows import (
936- blocking,
937 signal,
938 CredentialsManagement,
939 CredentialsManagementClient,
940+ LOCALHOST,
941 SignalBroadcaster,
942 SSOCredentials,
943 SSOCredentialsClient,
944@@ -41,7 +41,8 @@
945 SSOLoginClient,
946 UbuntuSSORoot,
947 UbuntuSSOClient,
948- get_sso_pb_hostport)
949+ get_sso_pb_port,
950+)
951
952 # because we are using twisted we have java like names C0103 and
953 # the issues that mocker brings with it like W0104
954@@ -191,10 +192,10 @@
955 self.sso_root = UbuntuSSORoot(self.login, None, None)
956 self.server_factory = SaveProtocolServerFactory(self.sso_root)
957 # pylint: disable=E1101
958- host, port = get_sso_pb_hostport()
959+ port = get_sso_pb_port()
960 self.listener = reactor.listenTCP(port, self.server_factory)
961 self.client_factory = PBClientFactory()
962- self.connector = reactor.connectTCP(host, port,
963+ self.connector = reactor.connectTCP(LOCALHOST, port,
964 self.client_factory)
965 # pylint: enable=E1101
966
967@@ -292,7 +293,7 @@
968 @defer.inlineCallbacks
969 def test_execution(client):
970 """Actual test."""
971- self.root.generate_captcha(app_name, filename, blocking,
972+ self.root.generate_captcha(app_name, filename,
973 self.login.emit_captcha_generated,
974 self.login.emit_captcha_generation_error)
975 self.mocker.replay()
976@@ -364,7 +365,7 @@
977 def test_execution(client):
978 """Actual test."""
979 self.root.register_user(app_name, email, password, displayname,
980- captcha_id, captcha_solution, blocking,
981+ captcha_id, captcha_solution,
982 self.login.emit_user_registered,
983 self.login.emit_user_registration_error)
984 self.mocker.replay()
985@@ -453,7 +454,7 @@
986 @defer.inlineCallbacks
987 def test_execution(client):
988 """Actual test."""
989- self.root.login(app_name, email, password, blocking,
990+ self.root.login(app_name, email, password,
991 self.login.emit_logged_in,
992 self.login.emit_login_error,
993 self.login.emit_user_not_validated)
994@@ -524,7 +525,7 @@
995 def test_execution(client):
996 """Actual test."""
997 self.root.validate_email(app_name, email, password, email_token,
998- blocking, self.login.emit_email_validated,
999+ self.login.emit_email_validated,
1000 self.login.emit_email_validation_error)
1001 self.mocker.replay()
1002 yield client.validate_email(app_name, email, password, email_token)
1003@@ -590,7 +591,7 @@
1004 @defer.inlineCallbacks
1005 def test_execution(client):
1006 """Actual test."""
1007- self.root.request_password_reset_token(app_name, email, blocking,
1008+ self.root.request_password_reset_token(app_name, email,
1009 self.login.emit_password_reset_token_sent,
1010 self.login.emit_password_reset_error)
1011 self.mocker.replay()
1012@@ -660,7 +661,7 @@
1013 def test_execution(client):
1014 """Actual test."""
1015 self.root.set_new_password(app_name, email, token, new_password,
1016- blocking, self.login.emit_password_changed,
1017+ self.login.emit_password_changed,
1018 self.login.emit_password_change_error)
1019 self.mocker.replay()
1020 yield client.set_new_password(app_name, email, token, new_password)
1021@@ -690,10 +691,10 @@
1022 self.sso_root = UbuntuSSORoot(None, self.creds, None)
1023 self.server_factory = SaveProtocolServerFactory(self.sso_root)
1024 # pylint: disable=E1101
1025- host, port = get_sso_pb_hostport()
1026+ port = get_sso_pb_port()
1027 self.listener = reactor.listenTCP(port, self.server_factory)
1028 self.client_factory = PBClientFactory()
1029- self.connector = reactor.connectTCP(host, port,
1030+ self.connector = reactor.connectTCP(LOCALHOST, port,
1031 self.client_factory)
1032 # pylint: enable=E1101
1033
1034@@ -945,10 +946,10 @@
1035 self.sso_root = UbuntuSSORoot(None, None, self.creds)
1036 self.server_factory = SaveProtocolServerFactory(self.sso_root)
1037 # pylint: disable=E1101
1038- host, port = get_sso_pb_hostport()
1039+ port = get_sso_pb_port()
1040 self.listener = reactor.listenTCP(port, self.server_factory)
1041 self.client_factory = PBClientFactory()
1042- self.connector = reactor.connectTCP(host, port,
1043+ self.connector = reactor.connectTCP(LOCALHOST, port,
1044 self.client_factory)
1045 # pylint: enable=E1101
1046
1047@@ -1257,6 +1258,25 @@
1048 # pylint: enable=E1101
1049 return d
1050
1051+ def test_login_email_password(self):
1052+ """Test that root is called."""
1053+ app_name = 'app'
1054+ args = 'args'
1055+
1056+ @defer.inlineCallbacks
1057+ def test_execution(client):
1058+ """Actual test."""
1059+ self.root.login_email_password(app_name, args)
1060+ self.mocker.replay()
1061+ yield client.login_email_password(app_name, args)
1062+ yield client.unregister_to_signals()
1063+
1064+ d = self._get_client()
1065+ # pylint: disable=E1101
1066+ d.addCallback(test_execution)
1067+ # pylint: enable=E1101
1068+ return d
1069+
1070
1071 class MockRemoteObject(object):
1072 """A mock RemoteObject."""
1073@@ -1330,7 +1350,6 @@
1074 self.assertEqual(connect_result, ussoc)
1075 self.assertTrue(mac.port_requested, "The port must be requested.")
1076 self.assertNotEqual(ussoc.sso_login, None)
1077- self.assertNotEqual(ussoc.sso_cred, None)
1078 self.assertNotEqual(ussoc.cred_management, None)
1079
1080 def test_get_activation_cmdline(self):
1081@@ -1340,3 +1359,69 @@
1082 self.patch(windows, "QueryValueEx", lambda key, v: (key, REG_SZ))
1083 cmdline = windows.get_activation_cmdline(windows.SSO_SERVICE_NAME)
1084 self.assertEqual(SAMPLE_VALUE, cmdline)
1085+
1086+
1087+class MockWin32APIs(object):
1088+ """Some mock win32apis."""
1089+
1090+ process_handle = object()
1091+ TOKEN_ALL_ACCESS = object()
1092+ TokenUser = object()
1093+
1094+ def __init__(self, sample_token):
1095+ """Initialize this mock instance."""
1096+ self.sample_token = sample_token
1097+ self.token_handle = object()
1098+
1099+ def GetCurrentProcess(self):
1100+ """Returns a fake process_handle."""
1101+ return self.process_handle
1102+
1103+ def OpenProcessToken(self, process_handle, access):
1104+ """Open the process token."""
1105+ assert process_handle is self.process_handle
1106+ assert access is self.TOKEN_ALL_ACCESS
1107+ return self.token_handle
1108+
1109+ def GetTokenInformation(self, token_handle, info):
1110+ """Get the information for this token."""
1111+ assert token_handle == self.token_handle
1112+ assert info == self.TokenUser
1113+ return (self.sample_token, 0)
1114+
1115+
1116+class AssortedTestCase(TestCase):
1117+ """Tests for functions in the windows module."""
1118+
1119+ def test_get_userid(self):
1120+ """The returned user id is parsed ok."""
1121+ expected_id = 1001
1122+ sample_token = "abc-123-1001"
1123+
1124+ win32apis = MockWin32APIs(sample_token)
1125+ self.patch(windows, "win32process", win32apis)
1126+ self.patch(windows, "win32security", win32apis)
1127+
1128+ userid = windows.get_user_id()
1129+ self.assertEqual(userid, expected_id)
1130+
1131+ def _test_port_assignation(self, uid, expected_port):
1132+ """Test a given uid/expected port combo."""
1133+ self.patch(windows, "get_user_id", lambda: uid)
1134+ self.assertEqual(windows.get_sso_pb_port(), expected_port)
1135+
1136+ def test_get_sso_pb_port(self):
1137+ """Test the get_sso_pb_port function."""
1138+ uid = 1001
1139+ uid_modulo = uid % windows.SSO_RESERVED_PORTS
1140+ expected_port = (windows.SSO_BASE_PB_PORT +
1141+ uid_modulo * windows.SSO_PORT_ALLOCATION_STEP)
1142+ self._test_port_assignation(uid, expected_port)
1143+
1144+ def test_get_sso_pb_port_alt(self):
1145+ """Test the get_sso_pb_port function."""
1146+ uid = 2011 + windows.SSO_RESERVED_PORTS
1147+ uid_modulo = uid % windows.SSO_RESERVED_PORTS
1148+ expected_port = (windows.SSO_BASE_PB_PORT +
1149+ uid_modulo * windows.SSO_PORT_ALLOCATION_STEP)
1150+ self._test_port_assignation(uid, expected_port)
1151
1152=== modified file 'ubuntu_sso/main/windows.py'
1153--- ubuntu_sso/main/windows.py 2011-07-22 20:12:20 +0000
1154+++ ubuntu_sso/main/windows.py 2011-08-25 19:16:19 +0000
1155@@ -17,11 +17,16 @@
1156 # with this program. If not, see <http://www.gnu.org/licenses/>.
1157 """Main implementation on windows."""
1158
1159+import warnings
1160+
1161 # pylint: disable=F0401
1162 from _winreg import HKEY_LOCAL_MACHINE, OpenKey, QueryValueEx
1163
1164 from functools import wraps
1165
1166+import win32process
1167+import win32security
1168+
1169 from twisted.internet import defer, reactor
1170 from twisted.internet.threads import deferToThread
1171 from twisted.spread.pb import (
1172@@ -44,19 +49,54 @@
1173 U1_REG_PATH = r'Software\Ubuntu One'
1174 SSO_INSTALL_PATH = 'SSOInstallPath'
1175 LOCALHOST = "127.0.0.1"
1176-SSO_DEFAULT_PB_PORT = 50001
1177+SSO_BASE_PB_PORT = 50000
1178+SSO_RESERVED_PORTS = 3000
1179+SSO_PORT_ALLOCATION_STEP = 3 # contiguous ports for sso, u1client, and u1cp
1180 SSO_SERVICE_NAME = "ubuntu-sso-client"
1181
1182
1183-def get_sso_pb_hostport():
1184- """Get the host and port on which the SSO pb is running."""
1185- return LOCALHOST, SSO_DEFAULT_PB_PORT
1186+def get_user_id():
1187+ """Find the numeric user id."""
1188+ process_handle = win32process.GetCurrentProcess()
1189+ token_handle = win32security.OpenProcessToken(process_handle,
1190+ win32security.TOKEN_ALL_ACCESS)
1191+ user_sid = win32security.GetTokenInformation(token_handle,
1192+ win32security.TokenUser)[0]
1193+ sid_parts = str(user_sid).split("-")
1194+ uid = int(sid_parts[-1])
1195+ return uid
1196+
1197+
1198+def get_sso_pb_port():
1199+ """Get the port on which the SSO pb is running."""
1200+ uid = get_user_id()
1201+ uid_modulo = uid % SSO_RESERVED_PORTS
1202+ port = SSO_BASE_PB_PORT + uid_modulo * SSO_PORT_ALLOCATION_STEP
1203+ return port
1204
1205
1206 def remote_handler(handler):
1207- """Execute a callback in a remote object."""
1208+ """Execute a callback in a remote object.
1209+
1210+ If the callback takes arguments, it's assumed that the last
1211+ one is a twisted Failure, and it has no keyword arguments.
1212+ """
1213 if handler:
1214- return lambda: handler.callRemote('execute')
1215+
1216+ def f(*args):
1217+ """Process arguments and call remote."""
1218+ try:
1219+ args = list(args)
1220+ if args:
1221+ args[-1] = except_to_errdict(args[-1].value)
1222+ return handler.callRemote('execute', *args)
1223+ # Yes, I want to catch everything
1224+ # pylint: disable=W0703
1225+ except Exception:
1226+ logger.exception("Remote handler argument processing error:")
1227+ return f
1228+
1229+ logger.warning("Remote handler got an empty handler.")
1230 return lambda: None
1231
1232
1233@@ -70,7 +110,7 @@
1234
1235 def get_activation_config():
1236 """Get the configuration to activate the sso service."""
1237- _, port = get_sso_pb_hostport()
1238+ port = get_sso_pb_port()
1239 service_name = SSO_SERVICE_NAME
1240 cmdline = get_activation_cmdline(service_name)
1241 return ActivationConfig(service_name, cmdline, port)
1242@@ -82,7 +122,8 @@
1243 # the calls in twisted will be called with the args in a diff order,
1244 # in order to follow the linux api, we swap them around with a lambda
1245 d.addCallback(lambda result, app: result_cb(app, result), app_name)
1246- d.addErrback(lambda err, app: error_cb(app, err), app_name)
1247+ d.addErrback(lambda err, app:
1248+ error_cb(app, except_to_errdict(err.value)), app_name)
1249
1250
1251 class RemoteMeta(type):
1252@@ -177,7 +218,7 @@
1253
1254 def generate_captcha(self, app_name, filename):
1255 """Call the matching method in the processor."""
1256- self.root.generate_captcha(app_name, filename, blocking,
1257+ self.root.generate_captcha(app_name, filename,
1258 self.emit_captcha_generated,
1259 self.emit_captcha_generation_error)
1260
1261@@ -199,7 +240,7 @@
1262 captcha_id, captcha_solution):
1263 """Call the matching method in the processor."""
1264 self.root.register_user(app_name, email, password, displayname,
1265- captcha_id, captcha_solution, blocking,
1266+ captcha_id, captcha_solution,
1267 self.emit_user_registered,
1268 self.emit_user_registration_error)
1269
1270@@ -225,7 +266,7 @@
1271
1272 def login(self, app_name, email, password):
1273 """Call the matching method in the processor."""
1274- self.root.login(app_name, email, password, blocking,
1275+ self.root.login(app_name, email, password,
1276 self.emit_logged_in, self.emit_login_error,
1277 self.emit_user_not_validated)
1278
1279@@ -246,7 +287,7 @@
1280 def validate_email(self, app_name, email, password, email_token):
1281 """Call the matching method in the processor."""
1282 self.root.validate_email(app_name, email, password, email_token,
1283- blocking, self.emit_email_validated,
1284+ self.emit_email_validated,
1285 self.emit_email_validation_error)
1286
1287 # request_password_reset_token signals
1288@@ -265,7 +306,7 @@
1289
1290 def request_password_reset_token(self, app_name, email):
1291 """Call the matching method in the processor."""
1292- self.root.request_password_reset_token(app_name, email, blocking,
1293+ self.root.request_password_reset_token(app_name, email,
1294 self.emit_password_reset_token_sent,
1295 self.emit_password_reset_error)
1296
1297@@ -286,7 +327,7 @@
1298 def set_new_password(self, app_name, email, token, new_password):
1299 """Call the matching method in the processor."""
1300 self.root.set_new_password(app_name, email, token, new_password,
1301- blocking, self.emit_password_changed,
1302+ self.emit_password_changed,
1303 self.emit_password_change_error)
1304
1305
1306@@ -420,6 +461,7 @@
1307 'register',
1308 'shutdown',
1309 'login',
1310+ 'login_email_password',
1311 ]
1312
1313 def __init__(self, timeout_func, shutdown_func, *args, **kwargs):
1314@@ -537,6 +579,10 @@
1315 """Get credentials if found else prompt GUI to login."""
1316 self.root.login(app_name, args)
1317
1318+ def login_email_password(self, app_name, args):
1319+ """Get credentials if found, else login."""
1320+ self.root.login_email_password(app_name, args)
1321+
1322
1323 class UbuntuSSORoot(object, Root):
1324 """Root object that exposes the diff referenceable objects."""
1325@@ -571,27 +617,31 @@
1326
1327 def remote(function):
1328 """Decorate the function to make the remote call."""
1329+
1330 @wraps(function)
1331- def remote_wrapper(*args, **kwargs):
1332+ def remote_wrapper(instance, *args, **kwargs):
1333 """Return the deferred for the remote call."""
1334- fixed_args = args[1:]
1335- logger.info('Performing %s as a remote call.', function.func_name)
1336- return args[0].remote.callRemote(function.func_name, *fixed_args,
1337- **kwargs)
1338+ fname = function.__name__
1339+ logger.info('Performing %s as a remote call.', fname)
1340+ result = instance.remote.callRemote(fname, *args, **kwargs)
1341+ return result
1342+
1343 return remote_wrapper
1344
1345
1346 def signal(function):
1347 """Decorate a function to perform the signal callback."""
1348+
1349 @wraps(function)
1350- def callback_wrapper(*args, **kwargs):
1351+ def callback_wrapper(instance, *args, **kwargs):
1352 """Return the result of the callback if present."""
1353- callback = getattr(args[0], function.func_name + '_cb', None)
1354+ fname = function.__name__
1355+ callback = getattr(instance, fname + '_cb', None)
1356 if callback is not None:
1357- fixed_args = args[1:]
1358- if not kwargs:
1359- return callback(*fixed_args)
1360- return callback(*fixed_args, **kwargs)
1361+ logger.info('Emitting remote signal for %s with callback %r.',
1362+ fname, callback)
1363+ return callback(*args, **kwargs)
1364+
1365 return callback_wrapper
1366
1367
1368@@ -618,10 +668,10 @@
1369 """Create a new instance."""
1370 self.cb = cb
1371
1372- def remote_execute(self):
1373+ def remote_execute(self, *args, **kwargs):
1374 """Execute the callback."""
1375 if self.cb:
1376- self.cb()
1377+ self.cb(*args, **kwargs)
1378
1379
1380 def callbacks(callbacks_indexes=None, callbacks_names=None):
1381@@ -755,7 +805,10 @@
1382
1383
1384 class SSOCredentialsClient(RemoteClient, Referenceable):
1385- """Client that can perform calls to the remote SSOCredentials object."""
1386+ """Deprecated client for the remote SSOCredentials object.
1387+
1388+ This class is deprecated!
1389+ """
1390
1391 __metaclass__ = RemoteMeta
1392
1393@@ -768,6 +821,8 @@
1394
1395 def __init__(self, remote_login):
1396 """Create a client for the cred API."""
1397+ warnings.warn("SSOCredentialsClient is deprecated.",
1398+ DeprecationWarning)
1399 super(SSOCredentialsClient, self).__init__(remote_login)
1400
1401 @signal
1402@@ -913,6 +968,10 @@
1403 def login(self, app_name, args):
1404 """Get credentials if found else prompt GUI to login."""
1405
1406+ @remote
1407+ def login_email_password(self, app_name, args):
1408+ """Get credentials if found else login."""
1409+
1410
1411 class UbuntuSSOClientException(Exception):
1412 """Raised when there are issues connecting to the process."""
1413@@ -923,7 +982,6 @@
1414
1415 def __init__(self):
1416 self.sso_login = None
1417- self.sso_cred = None
1418 self.cred_management = None
1419 self.factory = None
1420 self.client = None
1421@@ -934,8 +992,6 @@
1422 sso_login = yield root.callRemote('get_sso_login')
1423 logger.debug('SSOLogin is %s', sso_login)
1424 self.sso_login = SSOLoginClient(sso_login)
1425- sso_cred = yield root.callRemote('get_sso_credentials')
1426- self.sso_cred = SSOCredentialsClient(sso_cred)
1427 cred_management = yield root.callRemote('get_cred_manager')
1428 self.cred_management = CredentialsManagementClient(cred_management)
1429 defer.returnValue(self)
1430
1431=== modified file 'ubuntu_sso/networkstate/windows.py'
1432--- ubuntu_sso/networkstate/windows.py 2011-03-22 23:29:20 +0000
1433+++ ubuntu_sso/networkstate/windows.py 2011-08-25 19:16:19 +0000
1434@@ -102,9 +102,9 @@
1435
1436 def __init__(self, connected_cb=None, connected_cb_info=None,
1437 disconnected_cb=None):
1438- # pylint: disable=E1101
1439+ # pylint: disable=E1101,E1120,W0231
1440 self._wrap_(self)
1441- # pylint: enable=E1101
1442+ # pylint: enable=E1101,E1120
1443 self.connected_cb = connected_cb
1444 self.connected_cb_info = connected_cb_info
1445 self.disconnected_cb = disconnected_cb
1446@@ -131,6 +131,7 @@
1447 """Register to listen to network events."""
1448 # call the CoInitialize to allow the registration to run in another
1449 # thread
1450+ # pylint: disable=E1101
1451 pythoncom.CoInitialize()
1452 # interface to be used by com
1453 manager_interface = pythoncom.WrapObject(self)
1454@@ -157,6 +158,7 @@
1455 'Error registering %s to event %s', e, current_event[1])
1456
1457 pythoncom.PumpMessages()
1458+ # pylint: enable=E1101
1459
1460
1461 def is_machine_connected():
1462
1463=== modified file 'ubuntu_sso/qt/controllers.py'
1464--- ubuntu_sso/qt/controllers.py 2011-07-22 20:12:20 +0000
1465+++ ubuntu_sso/qt/controllers.py 2011-08-25 19:16:19 +0000
1466@@ -20,10 +20,12 @@
1467 import StringIO
1468 import tempfile
1469
1470+# pylint: disable=F0401
1471 try:
1472 from PIL import Image
1473 except ImportError:
1474 import Image
1475+# pylint: enable=F0401
1476
1477 from PyQt4.QtGui import QMessageBox, QWizard, QPixmap
1478 from PyQt4.QtCore import QUrl
1479@@ -34,6 +36,7 @@
1480 from ubuntu_sso.utils.ui import (
1481 CAPTCHA_LOAD_ERROR,
1482 CAPTCHA_SOLUTION_ENTRY,
1483+ CAPTCHA_REQUIRED_ERROR,
1484 EMAIL1_ENTRY,
1485 EMAIL2_ENTRY,
1486 EMAIL_LABEL,
1487@@ -76,14 +79,38 @@
1488 # disabled warnings about TODO comments
1489
1490
1491+# Based on the gtk implementation, but added error_message
1492+# support, and rewritten.
1493+def _build_general_error_message(errordict):
1494+ """Concatenate __all__ and message from the errordict."""
1495+ result = None
1496+ msg1 = errordict.get('__all__')
1497+ msg2 = errordict.get('message')
1498+ if msg2 is None:
1499+ # See the errordict in LP: 828417
1500+ msg2 = errordict.get('error_message')
1501+ if msg1 is not None and msg2 is not None:
1502+ result = '\n'.join((msg1, msg2))
1503+ elif msg1 is not None:
1504+ result = msg1
1505+ elif msg2 is not None:
1506+ result = msg2
1507+ else:
1508+ # I give up.
1509+ result = "Error: %r" % errordict
1510+ return result
1511+
1512+
1513 class BackendController(object):
1514 """Represent a controller that talks with the sso self.backend."""
1515
1516- def __init__(self):
1517+ def __init__(self, title='', subtitle=''):
1518 """Create a new instance."""
1519 self.root = None
1520 self.view = None
1521 self.backend = None
1522+ self._title = title
1523+ self._subtitle = subtitle
1524
1525 def __del__(self):
1526 """Clean the resources."""
1527@@ -105,22 +132,30 @@
1528 yield self.backend.register_to_signals()
1529 returnValue(self.backend)
1530
1531-
1532-class ChooseSignInController(object):
1533+ #pylint: disable=C0103
1534+ def pageInitialized(self):
1535+ """Call to prepare the page just before it is shown."""
1536+ pass
1537+ #pylint: enable=C0103
1538+
1539+
1540+class ChooseSignInController(BackendController):
1541 """Controlled to the ChooseSignIn view/widget."""
1542
1543- def __init__(self, title=''):
1544+ def __init__(self, title='', subtitle=''):
1545 """Create a new instance to manage the view."""
1546+ super(ChooseSignInController, self).__init__(title, subtitle)
1547 self.view = None
1548- self._title = title
1549
1550 # use an ugly name just so have a simlar api as found in PyQt
1551 #pylint: disable=C0103
1552+
1553 def setupUi(self, view):
1554 """Perform the required actions to set up the ui."""
1555 self.view = view
1556- self.view.setTitle(self._title)
1557 self._set_up_translated_strings()
1558+ self.view.header.set_title(self._title)
1559+ self.view.header.set_subtitle(self._subtitle)
1560 self._connect_buttons()
1561 #pylint: enable=C0103
1562
1563@@ -157,12 +192,10 @@
1564
1565 def __init__(self, backend=None, title='', subtitle='', message_box=None):
1566 """Create a new instance."""
1567- super(CurrentUserController, self).__init__()
1568+ super(CurrentUserController, self).__init__(title, subtitle)
1569 if message_box is None:
1570 message_box = QMessageBox
1571 self.message_box = message_box
1572- self._title = title
1573- self._subtitle = subtitle
1574
1575 def _set_translated_strings(self):
1576 """Set the translated strings."""
1577@@ -185,13 +218,24 @@
1578 self.backend.on_logged_in_cb = self.on_logged_in
1579 self.view.ui.forgot_password_label.linkActivated.connect(
1580 self.on_forgotten_password)
1581+ self.view.ui.email_edit.textChanged.connect(self._validate)
1582+ self.view.ui.password_edit.textChanged.connect(self._validate)
1583+
1584+ def _validate(self):
1585+ """Perform input validation."""
1586+ valid = True
1587+ if not is_correct_email(unicode(self.view.ui.email_edit.text())):
1588+ valid = False
1589+ elif not unicode(self.view.ui.password_edit.text()):
1590+ valid = False
1591+ self.view.ui.sign_in_button.setEnabled(valid)
1592
1593 def login(self):
1594 """Perform the login using the self.backend."""
1595 logger.debug('CurrentUserController.login')
1596 # grab the data from the view and call the backend
1597- email = str(self.view.ui.email_edit.text())
1598- password = str(self.view.ui.password_edit.text())
1599+ email = unicode(self.view.ui.email_edit.text())
1600+ password = unicode(self.view.ui.password_edit.text())
1601 d = self.backend.login(self.view.wizard().app_name, email, password)
1602 d.addErrback(self.on_login_error)
1603
1604@@ -200,13 +244,12 @@
1605 # let the user know
1606 logger.error('Got error when login %s, error: %s',
1607 self.view.wizard().app_name, error)
1608- self.message_box.critical(self.view, self.view.wizard().app_name,
1609- error['message'])
1610+ self.message_box.critical(_build_general_error_message(error))
1611
1612 def on_logged_in(self, app_name, result):
1613 """We managed to log in."""
1614 logger.info('Logged in for %s', app_name)
1615- email = str(self.view.ui.email_edit.text())
1616+ email = unicode(self.view.ui.email_edit.text())
1617 self.view.wizard().loginSuccess.emit(app_name, email)
1618 logger.debug('Wizard.loginSuccess emitted.')
1619
1620@@ -223,9 +266,8 @@
1621 """Setup the view."""
1622 self.view = view
1623 self.backend = yield self.get_backend()
1624- self.view.setTitle(self._title)
1625- if self._subtitle:
1626- self.view.setSubTitle(self._subtitle)
1627+ self.view.header.set_title(self._title)
1628+ self.view.header.set_subtitle(self._subtitle)
1629 self._set_translated_strings()
1630 self._connect_ui()
1631 #pylint: enable=C0103
1632@@ -234,9 +276,9 @@
1633 class SetUpAccountController(BackendController):
1634 """Conroller for the setup account view."""
1635
1636- def __init__(self, message_box=None):
1637+ def __init__(self, message_box=None, title='', subtitle=''):
1638 """Create a new instance."""
1639- super(SetUpAccountController, self).__init__()
1640+ super(SetUpAccountController, self).__init__(title, subtitle)
1641 if message_box is None:
1642 message_box = QMessageBox
1643 self.message_box = message_box
1644@@ -285,8 +327,6 @@
1645 logger.debug('SetUpAccountController._connect_ui_elements')
1646 self.view.ui.terms_button.clicked.connect(self.set_next_tos)
1647 self.view.ui.set_up_button.clicked.connect(self.set_next_validation)
1648- self.view.ui.terms_checkbox.stateChanged.connect(
1649- self.view.ui.set_up_button.setEnabled)
1650 self.view.ui.refresh_label.linkActivated.connect(lambda url: \
1651 self._refresh_captcha())
1652 # set the callbacks for the captcha generation
1653@@ -296,6 +336,38 @@
1654 self.backend.on_user_registered_cb = self.on_user_registered
1655 self.backend.on_user_registration_error_cb = \
1656 self.on_user_registration_error
1657+ # We need to check if we enable the button on many signals
1658+ self.view.ui.name_edit.textEdited.connect(self._enable_setup_button)
1659+ self.view.ui.email_edit.textEdited.connect(self._enable_setup_button)
1660+ self.view.ui.confirm_email_edit.textEdited.connect(
1661+ self._enable_setup_button)
1662+ self.view.ui.password_edit.textEdited.connect(
1663+ self._enable_setup_button)
1664+ self.view.ui.confirm_password_edit.textEdited.connect(
1665+ self._enable_setup_button)
1666+ self.view.ui.captcha_solution_edit.textEdited.connect(
1667+ self._enable_setup_button)
1668+ self.view.ui.terms_checkbox.stateChanged.connect(
1669+ self._enable_setup_button)
1670+
1671+ def _enable_setup_button(self):
1672+ """Only enable the setup button if the form is valid."""
1673+ email = unicode(self.view.ui.email_edit.text())
1674+ confirm_email = unicode(self.view.ui.confirm_email_edit.text())
1675+ password = unicode(self.view.ui.password_edit.text())
1676+ confirm_password = unicode(self.view.ui.confirm_password_edit.text())
1677+ captcha_solution = unicode(self.view.ui.captcha_solution_edit.text())
1678+
1679+ enabled = self.view.ui.terms_checkbox.isChecked() and \
1680+ captcha_solution and is_min_required_password(password) and \
1681+ password == confirm_password and is_correct_email(email) and \
1682+ email == confirm_email
1683+
1684+ self.view.ui.set_up_button.setEnabled(enabled)
1685+ self.view.ui.set_up_button.setProperty("DisabledState",
1686+ not self.view.ui.set_up_button.isEnabled())
1687+ self.view.ui.set_up_button.style().unpolish(self.view.ui.set_up_button)
1688+ self.view.ui.set_up_button.style().polish(self.view.ui.set_up_button)
1689
1690 def _refresh_captcha(self):
1691 """Refresh the captcha image shown in the ui."""
1692@@ -316,9 +388,9 @@
1693 def _set_titles(self):
1694 """Set the diff titles of the view."""
1695 logger.debug('SetUpAccountController._set_titles')
1696- wizard = self.view.wizard()
1697- self.view.setTitle(JOIN_HEADER_LABEL % {'app_name': wizard.app_name})
1698- self.view.setSubTitle(wizard.help_text)
1699+ self.view.header.set_title(
1700+ JOIN_HEADER_LABEL % {'app_name': self.view.wizard().app_name})
1701+ self.view.header.set_subtitle(self.view.wizard().help_text)
1702
1703 def _register_fields(self):
1704 """Register the diff fields of the Ui."""
1705@@ -348,19 +420,14 @@
1706 def on_captcha_generation_error(self, error):
1707 """An error ocurred."""
1708 logger.debug('SetUpAccountController.on_captcha_generation_error')
1709- self.message_box.critical(self.view, self.view.wizard().app_name,
1710- CAPTCHA_LOAD_ERROR)
1711+ self.message_box.critical(CAPTCHA_LOAD_ERROR)
1712
1713 def on_user_registration_error(self, app_name, error):
1714 """Let the user know we could not register."""
1715 logger.debug('SetUpAccountController.on_user_registration_error')
1716 # errors are returned as a dict with the data we want to show.
1717 self._refresh_captcha()
1718- errors = [v for _, v in sorted(error.iteritems())]
1719- self.message_box.critical(
1720- self.view,
1721- self.view.wizard().app_name,
1722- '\n'.join(errors))
1723+ self.message_box.critical(_build_general_error_message(error))
1724
1725 def on_user_registered(self, app_name, result):
1726 """Execute when the user did register."""
1727@@ -377,35 +444,35 @@
1728 def validate_form(self):
1729 """Validate the info of the form and return an error."""
1730 logger.debug('SetUpAccountController.validate_form')
1731- email = str(self.view.ui.email_edit.text())
1732- confirm_email = str(self.view.ui.confirm_email_edit.text())
1733- password = str(self.view.ui.password_edit.text())
1734- confirm_password = str(self.view.ui.confirm_password_edit.text())
1735+ email = unicode(self.view.ui.email_edit.text())
1736+ confirm_email = unicode(self.view.ui.confirm_email_edit.text())
1737+ password = unicode(self.view.ui.password_edit.text())
1738+ confirm_password = unicode(self.view.ui.confirm_password_edit.text())
1739+ captcha_solution = unicode(self.view.ui.captcha_solution_edit.text())
1740 if not is_correct_email(email):
1741- self.message_box.critical(self.view, self.view.wizard().app_name,
1742- EMAIL_INVALID)
1743+ self.message_box.critical(EMAIL_INVALID)
1744 if email != confirm_email:
1745- self.message_box.critical(self.view, self.view.wizard().app_name,
1746- EMAIL_MISMATCH)
1747+ self.message_box.critical(EMAIL_MISMATCH)
1748 return False
1749 if not is_min_required_password(password):
1750- self.message_box.critical(self.view, self.view.wizard().app_name,
1751- PASSWORD_TOO_WEAK)
1752+ self.message_box.critical(PASSWORD_TOO_WEAK)
1753 return False
1754 if password != confirm_password:
1755- self.message_box.critical(self.view, self.view.wizard().app_name,
1756- PASSWORD_MISMATCH)
1757+ self.message_box.critical(PASSWORD_MISMATCH)
1758+ return False
1759+ if not captcha_solution:
1760+ self.message_box.critical(CAPTCHA_REQUIRED_ERROR)
1761 return False
1762 return True
1763
1764 def set_next_validation(self):
1765 """Set the validation as the next page."""
1766 logger.debug('SetUpAccountController.set_next_validation')
1767- email = str(self.view.ui.email_edit.text())
1768- password = str(self.view.ui.password_edit.text())
1769- name = str(self.view.ui.name_edit.text())
1770+ email = unicode(self.view.ui.email_edit.text())
1771+ password = unicode(self.view.ui.password_edit.text())
1772+ name = unicode(self.view.ui.name_edit.text())
1773 captcha_id = self.view.captcha_id
1774- captcha_solution = str(self.view.ui.captcha_solution_edit.text())
1775+ captcha_solution = unicode(self.view.ui.captcha_solution_edit.text())
1776 # validate the current info of the form, try to perform the action
1777 # to register the user, and then move foward
1778 if self.validate_form():
1779@@ -424,12 +491,12 @@
1780 def is_correct_email_confirmation(self, email_address):
1781 """Return that the email is the same."""
1782 logger.debug('SetUpAccountController.is_correct_email_confirmation')
1783- return self.view.ui.email_edit.text() == email_address
1784+ return unicode(self.view.ui.email_edit.text()) == email_address
1785
1786 def is_correct_password_confirmation(self, password):
1787 """Return that the passwords are correct."""
1788 logger.debug('SetUpAccountController.is_correct_password_confirmation')
1789- return self.view.ui.password_edit.text() == password
1790+ return unicode(self.view.ui.password_edit.text()) == password
1791
1792 #pylint: disable=C0103
1793 @inlineCallbacks
1794@@ -441,28 +508,29 @@
1795 self._connect_ui_elements()
1796 self._refresh_captcha()
1797 self._set_titles()
1798+ self.view.header.set_title(self._title)
1799+ self.view.header.set_subtitle(self._subtitle)
1800 self._set_translated_strings()
1801 self._set_line_edits_validations()
1802 self._register_fields()
1803 #pylint: enable=C0103
1804
1805
1806-class TosController(object):
1807+class TosController(BackendController):
1808 """Controller used for the tos page."""
1809
1810 def __init__(self, title='', subtitle='', tos_url=''):
1811 """Create a new instance."""
1812+ super(TosController, self).__init__(title, subtitle)
1813 self.view = None
1814- self._title = title
1815- self._subtitle = subtitle
1816 self._tos_url = tos_url
1817
1818 #pylint: disable=C0103
1819 def setupUi(self, view):
1820 """Set up the ui."""
1821 self.view = view
1822- self.view.setTitle(self._title)
1823- self.view.setSubTitle(self._subtitle)
1824+ self.view.header.set_title(self._title)
1825+ self.view.header.set_subtitle(self._subtitle)
1826 # load the tos page
1827 self.view.ui.terms_webkit.load(QUrl(self._tos_url))
1828 self.view.ui.tos_link_label.setText(
1829@@ -474,6 +542,13 @@
1830 class EmailVerificationController(BackendController):
1831 """Controller used for the verification page."""
1832
1833+ def __init__(self, message_box=None, title='', subtitle=''):
1834+ """Create a new instance."""
1835+ super(EmailVerificationController, self).__init__(title, subtitle)
1836+ if message_box is None:
1837+ message_box = QMessageBox
1838+ self.message_box = message_box
1839+
1840 def _set_translated_strings(self):
1841 """Set the trnaslated strings."""
1842 logger.debug('EmailVerificationController._set_translated_strings')
1843@@ -492,11 +567,11 @@
1844 def _set_titles(self):
1845 """Set the different titles."""
1846 logger.debug('EmailVerificationController._set_titles')
1847- self.view.setTitle(VERIFY_EMAIL_TITLE)
1848- self.view.setSubTitle(VERIFY_EMAIL_CONTENT % {
1849+ self.view.header.set_title(VERIFY_EMAIL_TITLE)
1850+ self.view.header.set_subtitle(VERIFY_EMAIL_CONTENT % {
1851 "app_name": self.view.wizard().app_name,
1852 "email": self.view.wizard().field("email_address").toString(),
1853- })
1854+ })
1855
1856 def set_titles(self):
1857 """This class needs to have a public set_titles.
1858@@ -520,9 +595,9 @@
1859 def validate_email(self):
1860 """Call the next action."""
1861 logger.debug('EmailVerificationController.validate_email')
1862- email = str(self.view.wizard().field('email_address').toString())
1863- password = str(self.view.wizard().field('password').toString())
1864- code = str(self.view.ui.verification_code_edit.text())
1865+ email = unicode(self.view.wizard().field('email_address').toString())
1866+ password = unicode(self.view.wizard().field('password').toString())
1867+ code = unicode(self.view.ui.verification_code_edit.text())
1868 self.backend.validate_email(self.view.wizard().app_name, email,
1869 password, code)
1870
1871@@ -532,16 +607,30 @@
1872 email = self.view.wizard().field('email_address').toString()
1873 self.view.wizard().registrationSuccess.emit(app_name, email)
1874
1875- def on_email_validation_error(self, app_name, raised_error):
1876+ def on_email_validation_error(self, app_name, error):
1877 """Signal thrown when there's a problem validating the email."""
1878-
1879-
1880-class ErrorController(object):
1881+ msg = error.get('email_token')
1882+ if msg is None:
1883+ msg = _build_general_error_message(error)
1884+ self.message_box.critical(msg)
1885+
1886+ #pylint: disable=C0103
1887+ def pageInitialized(self):
1888+ """Called to prepare the page just before it is shown."""
1889+ self.view.next_button.setDefault(True)
1890+ self.view.next_button.style().unpolish(
1891+ self.view.next_button)
1892+ self.view.next_button.style().polish(
1893+ self.view.next_button)
1894+ #pylint: enable=C0103
1895+
1896+
1897+class ErrorController(BackendController):
1898 """Controller used for the error page."""
1899
1900- def __init__(self):
1901+ def __init__(self, title='', subtitle=''):
1902 """Create a new instance."""
1903- super(ErrorController, self).__init__()
1904+ super(ErrorController, self).__init__(title, subtitle)
1905 self.view = None
1906
1907 #pylint: disable=C0103
1908@@ -550,15 +639,21 @@
1909 self.view = view
1910 self.view.next = -1
1911 self.view.ui.error_message_label.setText(ERROR)
1912+ self.view.header.set_title(self._title)
1913+ self.view.header.set_subtitle(self._subtitle)
1914 #pylint: enable=C0103
1915
1916
1917 class ForgottenPasswordController(BackendController):
1918 """Controller used to deal with the forgotten pwd page."""
1919
1920- def __init__(self):
1921+ def __init__(self, message_box=None, title='', subtitle=''):
1922 """Create a new instance."""
1923 super(ForgottenPasswordController, self).__init__()
1924+ if message_box is None:
1925+ message_box = QMessageBox
1926+ self.message_box = message_box
1927+ super(ForgottenPasswordController, self).__init__(title, subtitle)
1928
1929 def _register_fields(self):
1930 """Register the fields of the wizard page."""
1931@@ -582,6 +677,7 @@
1932
1933 def _connect_ui(self):
1934 """Connect the diff signals from the Ui."""
1935+ self.view.email_address_line_edit.textChanged.connect(self._validate)
1936 self.view.send_button.clicked.connect(
1937 lambda: self.backend.request_password_reset_token(
1938 self.view.wizard().app_name,
1939@@ -592,6 +688,11 @@
1940 self.on_password_reset_token_sent()
1941 self.backend.on_password_reset_error_cb = self.on_password_reset_error
1942
1943+ def _validate(self):
1944+ """Validate that we have an email."""
1945+ email = unicode(self.view.email_address_line_edit.text())
1946+ self.view.send_button.setEnabled(is_correct_email(email))
1947+
1948 def on_try_again(self):
1949 """Set back the widget to the initial state."""
1950 self.view.error_label.setVisible(False)
1951@@ -632,15 +733,20 @@
1952 self._connect_ui()
1953 self._set_enhanced_line_edit()
1954 self._register_fields()
1955+ self.view.header.set_title(self._title)
1956+ self.view.header.set_subtitle(self._subtitle)
1957 #pylint: enable=C0103
1958
1959
1960 class ResetPasswordController(BackendController):
1961 """Controller used to deal with reseintg the password."""
1962
1963- def __init__(self):
1964+ def __init__(self, title='', subtitle='', message_box=None):
1965 """Create a new instance."""
1966- super(ResetPasswordController, self).__init__()
1967+ if message_box is None:
1968+ message_box = QMessageBox
1969+ self.message_box = message_box
1970+ super(ResetPasswordController, self).__init__(title, subtitle)
1971
1972 def _set_translated_strings(self):
1973 """Translate the diff strings used in the app."""
1974@@ -658,6 +764,25 @@
1975 self.backend.on_password_changed_cb = self.on_password_changed
1976 self.backend.on_password_change_error_cb = \
1977 self.on_password_change_error
1978+ self.view.ui.reset_code_line_edit.textChanged.connect(self._validate)
1979+ self.view.ui.password_line_edit.textChanged.connect(self._validate)
1980+ self.view.ui.confirm_password_line_edit.textChanged.connect(
1981+ self._validate)
1982+
1983+ def _validate(self):
1984+ """Enable the submit button if data is valid."""
1985+ enabled = True
1986+ code = unicode(self.view.ui.reset_code_line_edit.text())
1987+ password = unicode(self.view.ui.password_line_edit.text())
1988+ confirm_password = unicode(
1989+ self.view.ui.confirm_password_line_edit.text())
1990+ if not is_min_required_password(password):
1991+ enabled = False
1992+ elif not self.is_correct_password_confirmation(confirm_password):
1993+ enabled = False
1994+ elif not code:
1995+ enabled = False
1996+ self.view.ui.reset_password_button.setEnabled(enabled)
1997
1998 def _add_line_edits_validations(self):
1999 """Add the validations to be use by the line edits."""
2000@@ -676,20 +801,23 @@
2001
2002 def on_password_change_error(self, app_name, error):
2003 """Let the user know that there was an error."""
2004+ logger.error('Got error changing password for %s, error: %s',
2005+ self.view.wizard().app_name, error)
2006+ self.message_box.critical(_build_general_error_message(error))
2007
2008 def set_new_password(self):
2009 """Request a new password to be set."""
2010 app_name = self.view.wizard().app_name
2011- email = str(self.view.wizard().field('email_address').toString())
2012- code = str(self.view.ui.reset_code_line_edit.text())
2013- password = str(self.view.ui.password_line_edit.text())
2014+ email = unicode(self.view.wizard().field('email_address').toString())
2015+ code = unicode(self.view.ui.reset_code_line_edit.text())
2016+ password = unicode(self.view.ui.password_line_edit.text())
2017 logger.info('Settig new password for %s and email %s with code %s',
2018 app_name, email, code)
2019 self.backend.set_new_password(app_name, email, code, password)
2020
2021 def is_correct_password_confirmation(self, password):
2022 """Return if the password is correct."""
2023- return self.view.ui.password_line_edit.text() == password
2024+ return unicode(self.view.ui.password_line_edit.text()) == password
2025
2026 #pylint: disable=C0103
2027 @inlineCallbacks
2028@@ -700,14 +828,17 @@
2029 self._set_translated_strings()
2030 self._connect_ui()
2031 self._add_line_edits_validations()
2032+ self.view.header.set_title(self._title)
2033+ self.view.header.set_subtitle(self._subtitle)
2034 #pylint: enable=C0103
2035
2036
2037-class SuccessController(object):
2038+class SuccessController(BackendController):
2039 """Controller used for the success page."""
2040
2041- def __init__(self):
2042+ def __init__(self, title='', subtitle=''):
2043 """Create a new instance."""
2044+ super(SuccessController, self).__init__(title, subtitle)
2045 self.view = None
2046
2047 #pylint: disable=C0103
2048@@ -716,6 +847,8 @@
2049 self.view = view
2050 self.view.next = -1
2051 self.view.ui.success_message_label.setText(SUCCESS)
2052+ self.view.header.set_title(self._title)
2053+ self.view.header.set_subtitle(self._subtitle)
2054 #pylint: enable=C0103
2055
2056
2057@@ -741,7 +874,8 @@
2058 def on_login_success(self, app_name, email):
2059 """Process the success of a login."""
2060 logger.debug('UbuntuSSOWizardController.on_login_success')
2061- result = yield self.login_success_callback(str(app_name), str(email))
2062+ result = yield self.login_success_callback(
2063+ unicode(app_name), unicode(email))
2064 logger.debug('Result from callback is %s', result)
2065 if result == 0:
2066 logger.info('Success in calling the given success_callback')
2067@@ -754,8 +888,8 @@
2068 def on_registration_success(self, app_name, email):
2069 """Process the success of a registration."""
2070 logger.debug('UbuntuSSOWizardController.on_registration_success')
2071- result = yield self.registration_success_callback(str(app_name),
2072- str(email))
2073+ result = yield self.registration_success_callback(unicode(app_name),
2074+ unicode(email))
2075 # TODO: what to do?
2076 logger.debug('Result from callback is %s', result)
2077 if result == 0:
2078
2079=== modified file 'ubuntu_sso/qt/gui.py'
2080--- ubuntu_sso/qt/gui.py 2011-06-24 19:14:17 +0000
2081+++ ubuntu_sso/qt/gui.py 2011-08-25 19:16:19 +0000
2082@@ -17,15 +17,18 @@
2083 """Qt implementation of the UI."""
2084
2085 from PyQt4.QtCore import pyqtSignal
2086+from PyQt4.QtCore import Qt
2087 from PyQt4.QtGui import (
2088 QApplication,
2089+ QWidget,
2090 QCursor,
2091 QHBoxLayout,
2092+ QVBoxLayout,
2093 QPixmap,
2094- QPushButton,
2095 QStyle,
2096 QWizard,
2097- QWizardPage)
2098+ QWizardPage,
2099+ QLabel)
2100
2101 from ubuntu_sso.logger import setup_logging
2102 # pylint: disable=F0401,E0611
2103@@ -55,6 +58,40 @@
2104 logger = setup_logging('ubuntu_sso.gui')
2105
2106
2107+class Header(QWidget):
2108+ """Header Class for Title and Subtitle in all wizard pages."""
2109+
2110+ def __init__(self):
2111+ """Create a new instance."""
2112+ QWidget.__init__(self)
2113+ vbox = QVBoxLayout(self)
2114+ self.title_label = QLabel()
2115+ self.title_label.setWordWrap(True)
2116+ self.title_label.setObjectName('title_label')
2117+ self.subtitle_label = QLabel()
2118+ self.subtitle_label.setWordWrap(True)
2119+ vbox.addWidget(self.title_label)
2120+ vbox.addWidget(self.subtitle_label)
2121+ self.title_label.setVisible(False)
2122+ self.subtitle_label.setVisible(False)
2123+
2124+ def set_title(self, title):
2125+ """Set the Title of the page or hide it otherwise"""
2126+ if title:
2127+ self.title_label.setText(title)
2128+ self.title_label.setVisible(True)
2129+ else:
2130+ self.title_label.setVisible(False)
2131+
2132+ def set_subtitle(self, subtitle):
2133+ """Set the Subtitle of the page or hide it otherwise"""
2134+ if subtitle:
2135+ self.subtitle_label.setText(subtitle)
2136+ self.subtitle_label.setVisible(True)
2137+ else:
2138+ self.subtitle_label.setVisible(False)
2139+
2140+
2141 class SSOWizardPage(QWizardPage):
2142 """Root class for all wizard pages."""
2143
2144@@ -63,8 +100,11 @@
2145 QWizardPage.__init__(self, parent)
2146 self.ui = ui
2147 self.ui.setupUi(self)
2148- self.controller = controller
2149- self.controller.setupUi(self)
2150+ self.header = Header()
2151+ self.layout().insertWidget(0, self.header)
2152+ if controller:
2153+ self.controller = controller
2154+ self.controller.setupUi(self)
2155 self.next = -1
2156
2157 # pylint: disable=C0103
2158@@ -73,6 +113,25 @@
2159 return self.next
2160 # pylint: enable=C0103
2161
2162+ # pylint: disable=C0103
2163+ def initializePage(self):
2164+ """Called to prepare the page just before it is shown."""
2165+ if self.controller:
2166+ self.controller.pageInitialized()
2167+ # pylint: enable=C0103
2168+
2169+ # pylint: disable=C0103
2170+ def setTitle(self, title=''):
2171+ """Set the Wizard Page Title."""
2172+ self.header.set_title(title)
2173+ # pylint: enable=C0103
2174+
2175+ # pylint: disable=C0103
2176+ def setSubTitle(self, subtitle=''):
2177+ """Set the Wizard Page Subtitle."""
2178+ self.header.set_subtitle(subtitle)
2179+ # pylint: enable=C0103
2180+
2181
2182 class EnhancedLineEdit(object):
2183 """Represents and enhanced lineedit.
2184@@ -81,7 +140,8 @@
2185 that we are just adding extra items to it.
2186 """
2187
2188- def __init__(self, line_edit, valid_cb=lambda x: False):
2189+ def __init__(self, line_edit, valid_cb=lambda x: False,
2190+ warning_sign=False):
2191 """Create an instance."""
2192 self._line_edit = line_edit
2193 layout = QHBoxLayout(self._line_edit)
2194@@ -89,24 +149,27 @@
2195 self._line_edit.setLayout(layout)
2196 self.valid_cb = valid_cb
2197 layout.addStretch()
2198- self.clear_button = QPushButton(self._line_edit)
2199- layout.addWidget(self.clear_button)
2200- self.clear_button.setMinimumSize(16, 16)
2201- self.clear_button.setVisible(False)
2202- self.clear_button.setFlat(True)
2203- self.clear_button.setCursor(QCursor(0))
2204- self.clear_button.setIcon(QApplication.style().standardIcon(
2205- QStyle.SP_MessageBoxWarning))
2206+ self.clear_label = QLabel(self._line_edit)
2207+ self.clear_label.setMargin(2)
2208+ self.clear_label.setProperty("lineEditWarning", True)
2209+ layout.addWidget(self.clear_label)
2210+ self.clear_label.setMinimumSize(16, 16)
2211+ self.clear_label.setVisible(False)
2212+ self.clear_label.setCursor(QCursor(Qt.ArrowCursor))
2213+ if warning_sign:
2214+ icon = QApplication.style().standardIcon(
2215+ QStyle.SP_MessageBoxWarning)
2216+ self.clear_label.setPixmap(icon.pixmap(16, 16))
2217 # connect the change of text to the cation that will check if the
2218 # text is valid and if the icon should be shown.
2219 self._line_edit.textChanged.connect(self.show_button)
2220
2221 def show_button(self, string):
2222 """Decide if we show the button or not."""
2223- if not self.valid_cb(string):
2224- self.clear_button.setVisible(True)
2225+ if not self.valid_cb(string) and self.clear_label.pixmap() is not None:
2226+ self.clear_label.setVisible(True)
2227 else:
2228- self.clear_button.setVisible(False)
2229+ self.clear_label.setVisible(False)
2230
2231
2232 class SSOWizardEnhancedEditPage(SSOWizardPage):
2233
2234=== added file 'ubuntu_sso/qt/tests/login_u_p.py'
2235--- ubuntu_sso/qt/tests/login_u_p.py 1970-01-01 00:00:00 +0000
2236+++ ubuntu_sso/qt/tests/login_u_p.py 2011-08-25 19:16:19 +0000
2237@@ -0,0 +1,54 @@
2238+# -*- coding: utf-8 -*-
2239+# Author: Manuel de la Pena <manuel@canonical.com>
2240+#
2241+# Copyright 2011 Canonical Ltd.
2242+#
2243+# This program is free software: you can redistribute it and/or modify it
2244+# under the terms of the GNU General Public License version 3, as published
2245+# by the Free Software Foundation.
2246+#
2247+# This program is distributed in the hope that it will be useful, but
2248+# WITHOUT ANY WARRANTY; without even the implied warranties of
2249+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
2250+# PURPOSE. See the GNU General Public License for more details.
2251+#
2252+# You should have received a copy of the GNU General Public License along
2253+# with this program. If not, see <http://www.gnu.org/licenses/>.
2254+"""Script that shows the qt gui."""
2255+
2256+# pylint: disable=F0401, E1101
2257+from twisted.internet import reactor
2258+from twisted.internet.defer import inlineCallbacks
2259+from ubuntu_sso.main.windows import UbuntuSSOClient
2260+
2261+
2262+def found(*args):
2263+ """The credentials were found."""
2264+ print "creds found", args
2265+ reactor.stop()
2266+
2267+
2268+def errback(*args):
2269+ """Unsuccessful login."""
2270+ print "EB:", args
2271+
2272+
2273+@inlineCallbacks
2274+def main():
2275+ """Perform a client request to be logged in."""
2276+ client = UbuntuSSOClient()
2277+ client = yield client.connect()
2278+ yield client.cred_management.register_to_signals()
2279+ client.cred_management.on_credentials_found_cb = found
2280+ client.cred_management.on_credentials_error_cb = errback
2281+ yield client.cred_management.login_email_password('SUPER', dict(
2282+ app_name='SUPER',
2283+ email='xyz@canonical.com',
2284+ password='ABC',
2285+ ))
2286+ print "called ok"
2287+
2288+
2289+if __name__ == '__main__':
2290+ main()
2291+ reactor.run()
2292
2293=== modified file 'ubuntu_sso/qt/tests/show_gui.py'
2294--- ubuntu_sso/qt/tests/show_gui.py 2011-07-22 20:12:20 +0000
2295+++ ubuntu_sso/qt/tests/show_gui.py 2011-08-25 19:16:19 +0000
2296@@ -20,6 +20,12 @@
2297 from twisted.internet import reactor
2298 from twisted.internet.defer import inlineCallbacks
2299 from ubuntu_sso.main.windows import UbuntuSSOClient
2300+from ubuntu_sso.credentials import (
2301+ TC_URL_KEY,
2302+ HELP_TEXT_KEY,
2303+ WINDOW_ID_KEY,
2304+ UI_MODULE_KEY,
2305+)
2306
2307
2308 def found(*args):
2309@@ -33,11 +39,16 @@
2310 """Perform a client request to be logged in."""
2311 client = UbuntuSSOClient()
2312 client = yield client.connect()
2313- client.sso_cred.on_credentials_found_cb = found
2314- yield client.sso_cred.register_to_signals()
2315- yield client.sso_cred.login_or_register_to_get_credentials('Ubuntu One',
2316- 'http://www.google.com',
2317- 'This is a test.', 0)
2318+ client.cred_management.on_credentials_found_cb = found
2319+ yield client.cred_management.register_to_signals()
2320+ yield client.cred_management.login(
2321+ 'Ubuntu One',
2322+ {
2323+ TC_URL_KEY: 'http://www.google.com',
2324+ HELP_TEXT_KEY: 'This is a test.',
2325+ WINDOW_ID_KEY: 0,
2326+ UI_MODULE_KEY: 'ubuntu_sso.qt.gui',
2327+ })
2328 print "called ok"
2329
2330
2331
2332=== added file 'ubuntu_sso/qt/tests/test_enchanced_line_edit.py'
2333--- ubuntu_sso/qt/tests/test_enchanced_line_edit.py 1970-01-01 00:00:00 +0000
2334+++ ubuntu_sso/qt/tests/test_enchanced_line_edit.py 2011-08-25 19:16:19 +0000
2335@@ -0,0 +1,94 @@
2336+# -*- coding: utf-8 -*-
2337+# Author: Diego Sarmentero <diego.sarmentero@canonical.com>
2338+#
2339+# Copyright 2011 Canonical Ltd.
2340+#
2341+# This program is free software: you can redistribute it and/or modify it
2342+# under the terms of the GNU General Public License version 3, as published
2343+# by the Free Software Foundation.
2344+#
2345+# This program is distributed in the hope that it will be useful, but
2346+# WITHOUT ANY WARRANTY; without even the implied warranties of
2347+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
2348+# PURPOSE. See the GNU General Public License for more details.
2349+#
2350+# You should have received a copy of the GNU General Public License along
2351+# with this program. If not, see <http://www.gnu.org/licenses/>.
2352+"""Test the EnhancedLineEdit."""
2353+
2354+import collections
2355+
2356+from twisted.trial.unittest import TestCase
2357+
2358+from PyQt4 import QtGui
2359+from PyQt4 import QtCore
2360+
2361+from ubuntu_sso.qt.gui import EnhancedLineEdit
2362+
2363+
2364+class EnhancedLineEditTestCase(TestCase):
2365+ """Tests EnhancedLineEdit for valid and invalid states."""
2366+
2367+ def get_pixmap_data(self, pixmap):
2368+ """Get the raw data of a QPixmap."""
2369+ byte_array = QtCore.QByteArray()
2370+ array_buffer = QtCore.QBuffer(byte_array)
2371+ pixmap.save(array_buffer, "PNG")
2372+ return byte_array
2373+
2374+ # pylint: disable=C0103
2375+ def assertEqualPixmaps(self, pixmap1, pixmap2):
2376+ """Compare two Qt pixmaps."""
2377+ d1 = self.get_pixmap_data(pixmap1)
2378+ d2 = self.get_pixmap_data(pixmap2)
2379+ self.assertEqual(d1, d2)
2380+ # pylint: enable=C0103
2381+
2382+ def test_enhanced_line_edit_init(self):
2383+ """Tests the initial state of the enchanced line edit."""
2384+ line_edit = QtGui.QLineEdit()
2385+ line_edit.show()
2386+ enhanced = EnhancedLineEdit(line_edit)
2387+ self.assertIsInstance(enhanced.valid_cb, collections.Callable)
2388+ self.assertTrue(enhanced.clear_label.property("lineEditWarning"))
2389+ self.assertFalse(enhanced.clear_label.isVisible())
2390+ self.assertEqual(enhanced.clear_label.cursor().shape(),
2391+ QtCore.Qt.ArrowCursor)
2392+ self.assertFalse(enhanced.clear_label.isVisible())
2393+ self.assertEqual(enhanced.clear_label.pixmap(), None)
2394+
2395+ def test_enhanced_line_edit_init_with_icon(self):
2396+ """Tests the initial state of the enchanced line edit."""
2397+ line_edit = QtGui.QLineEdit()
2398+ line_edit.show()
2399+ enhanced = EnhancedLineEdit(line_edit, warning_sign=True)
2400+ self.assertIsInstance(enhanced.valid_cb, collections.Callable)
2401+ edit_property = enhanced.clear_label.property("lineEditWarning")
2402+ self.assertTrue(edit_property)
2403+ self.assertFalse(enhanced.clear_label.isVisible())
2404+ self.assertEqual(enhanced.clear_label.cursor().shape(),
2405+ QtCore.Qt.ArrowCursor)
2406+ self.assertFalse(enhanced.clear_label.isVisible())
2407+ icon = QtGui.QApplication.style().standardIcon(
2408+ QtGui.QStyle.SP_MessageBoxWarning)
2409+ self.assertEqualPixmaps(enhanced.clear_label.pixmap(),
2410+ icon.pixmap(16, 16))
2411+
2412+ def test_enhanced_line_edit_function_show_true(self):
2413+ """Tests the EnchancedLineEdit show method with validation = True."""
2414+ line_edit = QtGui.QLineEdit()
2415+ line_edit.show()
2416+ func = lambda x: False
2417+ enhanced = EnhancedLineEdit(line_edit, valid_cb=func,
2418+ warning_sign=True)
2419+ line_edit.setText("testing")
2420+ self.assertTrue(enhanced.clear_label.isVisible())
2421+
2422+ def test_enhanced_line_edit_function_show_false(self):
2423+ """Tests the EnchancedLineEdit show method with validation = False."""
2424+ line_edit = QtGui.QLineEdit()
2425+ line_edit.show()
2426+ func = lambda x: True
2427+ enhanced = EnhancedLineEdit(line_edit, valid_cb=func)
2428+ line_edit.setText("testing")
2429+ self.assertFalse(enhanced.clear_label.isVisible())
2430
2431=== modified file 'ubuntu_sso/qt/tests/test_qt_views.py'
2432--- ubuntu_sso/qt/tests/test_qt_views.py 2011-06-24 19:14:17 +0000
2433+++ ubuntu_sso/qt/tests/test_qt_views.py 2011-08-25 19:16:19 +0000
2434@@ -24,6 +24,7 @@
2435 CurrentUserSignInPage,
2436 EmailVerificationPage,
2437 ErrorPage,
2438+ Header,
2439 SetupAccountPage,
2440 SuccessPage,
2441 TosPage)
2442@@ -72,7 +73,7 @@
2443 ui.setupUi(MATCH(lambda x: isinstance(x, ChooseSignInPage)))
2444 controller.setupUi(MATCH(lambda x: isinstance(x, ChooseSignInPage)))
2445 mocker.replay()
2446- self.widget = ChooseSignInPage(ui, controller)
2447+ self.widget = ChooseSignInPage(self.ui, self.controller)
2448
2449 def test_next_id(self):
2450 """Test the nextId method."""
2451@@ -217,3 +218,45 @@
2452 controller.setupUi(MATCH(lambda x: isinstance(x, ErrorPage)))
2453 mocker.replay()
2454 self.widget = ErrorPage(self.ui, self.controller)
2455+
2456+
2457+class HeaderTest(TestCase):
2458+ """Tests for injected Header in each Wizard Page."""
2459+
2460+ def setUp(self):
2461+ """Setup test."""
2462+ super(HeaderTest, self).setUp()
2463+ self.header = Header()
2464+
2465+ def test_label_state(self):
2466+ """Check the title and subtitle properties."""
2467+ self.assertTrue(self.header.title_label.wordWrap())
2468+ self.assertTrue(self.header.subtitle_label.wordWrap())
2469+ self.assertFalse(self.header.title_label.isVisible())
2470+ self.assertFalse(self.header.subtitle_label.isVisible())
2471+
2472+ def test_set_title(self):
2473+ """Check if set_title works ok, showing the widget if necessary."""
2474+ self.header.set_title('title')
2475+ self.assertEqual(self.header.title_label.text(), 'title')
2476+ self.header.show()
2477+ self.assertTrue(self.header.title_label.isVisible())
2478+ self.header.hide()
2479+
2480+ def test_set_empty_title(self):
2481+ """Check if the widget is hidden for empty title."""
2482+ self.header.set_title('')
2483+ self.assertFalse(self.header.title_label.isVisible())
2484+
2485+ def test_set_subtitle(self):
2486+ """Check if set_subtitle works ok, showing the widget if necessary."""
2487+ self.header.set_subtitle('subtitle')
2488+ self.assertEqual(self.header.subtitle_label.text(), 'subtitle')
2489+ self.header.show()
2490+ self.assertTrue(self.header.subtitle_label.isVisible())
2491+ self.header.hide()
2492+
2493+ def test_set_empty_subtitle(self):
2494+ """Check if the widget is hidden for empty subtitle."""
2495+ self.header.set_title('')
2496+ self.assertFalse(self.header.title_label.isVisible())
2497
2498=== modified file 'ubuntu_sso/qt/tests/test_windows.py'
2499--- ubuntu_sso/qt/tests/test_windows.py 2011-07-22 20:12:20 +0000
2500+++ ubuntu_sso/qt/tests/test_windows.py 2011-08-25 19:16:19 +0000
2501@@ -35,6 +35,7 @@
2502 UbuntuSSOWizardController)
2503 from ubuntu_sso.utils.ui import (
2504 CAPTCHA_SOLUTION_ENTRY,
2505+ CAPTCHA_REQUIRED_ERROR,
2506 EMAIL1_ENTRY,
2507 EMAIL2_ENTRY,
2508 EMAIL_MISMATCH,
2509@@ -69,7 +70,7 @@
2510 is_correct_email)
2511
2512 #ignore the comon mocker issues with lint
2513-# pylint: disable=W0212,W0104,W0106
2514+# pylint: disable=W0212,W0104,W0106,E1103
2515
2516
2517 class ChooseSignInControllerTestCase(MockerTestCase):
2518@@ -83,6 +84,19 @@
2519 self.controller = ChooseSignInController()
2520 self.controller.view = self.view
2521 self.controller.backend = self.backend
2522+ self.title = 'title'
2523+ self.subtitle = 'sub'
2524+
2525+ def test_setup_ui(self):
2526+ """Test the set up of the ui."""
2527+ self.controller._title = self.title
2528+ self.controller._subtitle = self.subtitle
2529+ self.controller._set_up_translated_strings()
2530+ self.view.header.set_title(self.title)
2531+ self.view.header.set_subtitle(self.subtitle)
2532+ self.controller._connect_buttons()
2533+ self.mocker.replay()
2534+ self.controller.setupUi(self.view)
2535
2536 def test_set_up_translated_strings(self):
2537 """Ensure that the translations are used."""
2538@@ -156,6 +170,8 @@
2539 self.backend.on_logged_in_cb = MATCH(callable)
2540 self.view.ui.forgot_password_label.linkActivated.connect(
2541 MATCH(callable))
2542+ self.view.ui.email_edit.textChanged.connect(MATCH(callable))
2543+ self.view.ui.password_edit.textChanged.connect(MATCH(callable))
2544 self.mocker.replay()
2545 self.controller._connect_ui()
2546
2547@@ -166,6 +182,227 @@
2548 self.assertEqual(self.controller._subtitle, 'the subtitle')
2549
2550
2551+class FakeLineEdit(object):
2552+
2553+ """A fake QLineEdit."""
2554+
2555+ def __init__(self, *args):
2556+ """Initialize."""
2557+ self._text = u""
2558+
2559+ def text(self):
2560+ """Save text."""
2561+ return self._text
2562+
2563+ # setText is inherited
2564+ # pylint: disable=C0103
2565+ def setText(self, text):
2566+ """Return saved text."""
2567+ self._text = text
2568+
2569+
2570+class FakePage(object):
2571+
2572+ """A fake wizard page."""
2573+
2574+ app_name = "APP"
2575+
2576+ def wizard(self):
2577+ """Fake wizard."""
2578+ return self
2579+
2580+
2581+class FakeCurrentUserPage(FakePage):
2582+
2583+ """Fake CurrentuserPage."""
2584+
2585+ def __init__(self, *args):
2586+ """Initialize."""
2587+ super(FakeCurrentUserPage, self).__init__(*args)
2588+ self.ui = self
2589+ self.ui.email_edit = FakeLineEdit()
2590+ self.ui.password_edit = FakeLineEdit()
2591+ self.sign_in_button = self
2592+ self._enabled = False
2593+
2594+ # setEnabled is inherited
2595+ # pylint: disable=C0103
2596+ def setEnabled(self, value):
2597+ """Fake setEnabled."""
2598+ self._enabled = value
2599+
2600+ def enabled(self):
2601+ """Fake enabled."""
2602+ return self._enabled
2603+
2604+
2605+class CurrentUserControllerValidationMockTest(MockerTestCase):
2606+ """Test the choose sign in controller."""
2607+
2608+ def setUp(self):
2609+ """Set tests."""
2610+ super(CurrentUserControllerValidationMockTest, self).setUp()
2611+ self.view = self.mocker.mock()
2612+ self.backend = self.mocker.mock()
2613+ self.controller = CurrentUserController()
2614+ self.controller.view = self.view
2615+ self.controller.backend = self.backend
2616+ self.title = 'title'
2617+ self.subtitle = 'sub'
2618+
2619+ def test_setup_ui(self):
2620+ """Test the set up of the ui."""
2621+ self.controller._title = self.title
2622+ self.controller._subtitle = self.subtitle
2623+ self.backend = yield self.controller.get_backend()
2624+ self.view.header.set_title(self.title)
2625+ self.view.header.set_subtitle(self.subtitle)
2626+ self.controller._set_translated_strings()
2627+ self.mocker.replay()
2628+ self.controller.setupUi(self.view)
2629+
2630+
2631+class CurrentUserControllerErrorTestCase(TestCase):
2632+
2633+ """Tests for CurrentUserController's error handler."""
2634+
2635+ on_error_method_name = "on_login_error"
2636+ controller_class = CurrentUserController
2637+
2638+ def setUp(self):
2639+ """Setup test."""
2640+ super(CurrentUserControllerErrorTestCase, self).setUp()
2641+ self.message_box = FakeMessageBox()
2642+ self.controller = self.controller_class(
2643+ message_box=self.message_box)
2644+ self.controller.view = FakePage()
2645+ self._called = False
2646+ self.on_error_method = getattr(
2647+ self.controller, self.on_error_method_name)
2648+
2649+ def test_error_message_key(self):
2650+ """Test that on_login_error reacts to errors with "error_message"."""
2651+ self.on_error_method({"error_message": "WORRY!"})
2652+ self.assertEqual(self.message_box.critical_args, (('WORRY!',), {}))
2653+
2654+ def test_message_key(self):
2655+ """Test that on_login_error reacts to errors with "message"."""
2656+ self.on_error_method({"message": "WORRY!"})
2657+ self.assertEqual(self.message_box.critical_args, (('WORRY!',), {}))
2658+
2659+ def test_broken_error(self):
2660+ """Test that on_login_error reacts to broken errors."""
2661+ self.on_error_method({"boo!": "WORRY!"})
2662+ self.assertEqual(self.message_box.critical_args,
2663+ (("Error: {'boo!': 'WORRY!'}",), {}))
2664+
2665+ def test_all_and_message(self):
2666+ """Test that on_login_error reacts to broken errors."""
2667+ self.on_error_method(
2668+ {"message": "WORRY!", "__all__": "MORE!"})
2669+ self.assertEqual(self.message_box.critical_args,
2670+ (('MORE!\nWORRY!',), {}))
2671+
2672+ def test_all_and_error_message(self):
2673+ """Test that on_login_error reacts to broken errors."""
2674+ self.on_error_method(
2675+ {"error_message": "WORRY!", "__all__": "MORE!"})
2676+ self.assertEqual(self.message_box.critical_args,
2677+ (('MORE!\nWORRY!',), {}))
2678+
2679+ def test_only_all(self):
2680+ """Test that on_login_error reacts to broken errors."""
2681+ self.on_error_method(
2682+ {"__all__": "MORE!"})
2683+ self.assertEqual(self.message_box.critical_args,
2684+ (('MORE!',), {}))
2685+
2686+
2687+class EmailVerificationControllerErrorTestCase(
2688+ CurrentUserControllerErrorTestCase):
2689+
2690+ """Tests for EmailVerificationController's error handler."""
2691+
2692+ on_error_method_name = "on_email_validation_error"
2693+ controller_class = EmailVerificationController
2694+
2695+ def setUp(self):
2696+ """Setup test."""
2697+ super(EmailVerificationControllerErrorTestCase, self).setUp()
2698+ # This error handler takes one extra argument.
2699+ self.on_error_method = lambda error: getattr(
2700+ self.controller, self.on_error_method_name)('APP', error)
2701+
2702+
2703+class SetUpAccountControllerErrorTestCase(
2704+ EmailVerificationControllerErrorTestCase):
2705+
2706+ """Tests for SetUpAccountController's error handler."""
2707+
2708+ on_error_method_name = "on_user_registration_error"
2709+ controller_class = SetUpAccountController
2710+
2711+ def setUp(self):
2712+ """Setup test."""
2713+ super(SetUpAccountControllerErrorTestCase, self).setUp()
2714+ self.patch(self.controller, "_refresh_captcha", lambda *args: None)
2715+
2716+
2717+class ResetPasswordControllerErrorTestCase(
2718+ EmailVerificationControllerErrorTestCase):
2719+
2720+ """Tests for ResetPasswordController's error handler."""
2721+
2722+ on_error_method_name = "on_password_change_error"
2723+ controller_class = ResetPasswordController
2724+
2725+
2726+class CurrentUserControllerValidationTest(TestCase):
2727+
2728+ """Tests for CurrentUserController, but without Mocker."""
2729+
2730+ def setUp(self):
2731+ """Setup test."""
2732+ super(CurrentUserControllerValidationTest, self).setUp()
2733+ self.message_box = FakeMessageBox()
2734+ self.controller = CurrentUserController(
2735+ message_box=self.message_box)
2736+ self.controller.view = FakeCurrentUserPage()
2737+ self._called = False
2738+
2739+ def _set_called(self, *args, **kwargs):
2740+ """Store 'args' and 'kwargs' for test assertions."""
2741+ self._called = (args, kwargs)
2742+
2743+ def test_valid(self):
2744+ """Enable the button with a valid email/password."""
2745+ self.controller.view.email_edit.setText("a@b")
2746+ self.controller.view.password_edit.setText("pass")
2747+ self.controller._validate()
2748+ self.assertTrue(self.controller.view.sign_in_button.enabled())
2749+
2750+ def test_invalid_email(self):
2751+ """The submit button should be disabled with an invalid email."""
2752+ self.controller.view.email_edit.setText("ab")
2753+ self.controller.view.password_edit.setText("pass")
2754+ self.controller._validate()
2755+ self.assertFalse(self.controller.view.sign_in_button.enabled())
2756+
2757+ def test_invalid_password(self):
2758+ """The submit button should be disabled with an invalid password."""
2759+ self.controller.view.email_edit.setText("a@b")
2760+ self.controller.view.password_edit.setText("")
2761+ self.controller._validate()
2762+ self.assertFalse(self.controller.view.sign_in_button.enabled())
2763+
2764+ def test_invalid_both(self):
2765+ """The submit button should be disabled with invalid data."""
2766+ self.controller.view.email_edit.setText("ab")
2767+ self.controller.view.password_edit.setText("")
2768+ self.controller._validate()
2769+ self.assertFalse(self.controller.view.sign_in_button.enabled())
2770+
2771+
2772 class SetUpAccountControllerTestCase(MockerTestCase):
2773 """test the controller used to setup a new account."""
2774
2775@@ -204,14 +441,13 @@
2776
2777 def test_set_titles(self):
2778 """Test how the different titles are set."""
2779- self.view.wizard()
2780- self.mocker.result(self.view)
2781- self.view.app_name
2782+ self.view.wizard().app_name
2783 self.mocker.result(self.app_name)
2784- self.view.help_text
2785+ self.view.wizard().help_text
2786 self.mocker.result(self.help)
2787- self.view.setTitle(JOIN_HEADER_LABEL % {'app_name': self.app_name})
2788- self.view.setSubTitle(self.help)
2789+ self.view.header.set_title(
2790+ JOIN_HEADER_LABEL % {'app_name': self.app_name})
2791+ self.view.header.set_subtitle(self.help)
2792 self.mocker.replay()
2793 self.controller._set_titles()
2794
2795@@ -220,11 +456,17 @@
2796 self.view.ui.terms_button.clicked.connect(self.controller.set_next_tos)
2797 self.view.ui.set_up_button.clicked.connect(
2798 self.controller.set_next_validation)
2799- self.view.ui.set_up_button.setEnabled
2800+ self.controller._enable_setup_button
2801 self.mocker.result(lambda: None)
2802+ self.view.ui.name_edit.textEdited.connect(MATCH(callable))
2803+ self.view.ui.email_edit.textEdited.connect(MATCH(callable))
2804+ self.view.ui.confirm_email_edit.textEdited.connect(MATCH(callable))
2805+ self.view.ui.password_edit.textEdited.connect(MATCH(callable))
2806+ self.view.ui.confirm_password_edit.textEdited.connect(MATCH(callable))
2807+ self.view.ui.captcha_solution_edit.textEdited.connect(MATCH(callable))
2808 self.view.ui.terms_checkbox.stateChanged.connect(MATCH(callable))
2809 self.view.ui.refresh_label.linkActivated.connect(MATCH(callable))
2810- # set the callbacks for the captcha generatio
2811+ # set the callbacks for the captcha generation
2812 self.backend.on_captcha_generated_cb = MATCH(callable)
2813 self.backend.on_captcha_generation_error_cb = MATCH(callable)
2814 self.backend.on_user_registration_error_cb = MATCH(callable)
2815@@ -309,6 +551,7 @@
2816 captcha_id, captcha_solution)
2817 self.view.wizard().email_verification_page_id
2818 self.view.wizard().page(None).controller.set_titles()
2819+ self.view.ui.captcha_solution_edit.text()
2820 self.mocker.replay()
2821 self.controller.set_next_validation()
2822
2823@@ -316,8 +559,7 @@
2824 """Test the callback when there is a wrong email."""
2825 email = 'email@example.com'
2826 password = 'Qwerty9923'
2827- self.view.wizard().app_name
2828- self.mocker.result(self.app_name)
2829+ captcha_solution = 'captcha'
2830 self.view.ui.email_edit.text()
2831 self.mocker.result(email)
2832 self.view.ui.confirm_email_edit.text()
2833@@ -326,7 +568,9 @@
2834 self.mocker.result(password)
2835 self.view.ui.confirm_password_edit.text()
2836 self.mocker.result(password)
2837- self.message_box.critical(self.view, self.app_name, EMAIL_MISMATCH)
2838+ self.view.ui.captcha_solution_edit.text()
2839+ self.mocker.result(captcha_solution)
2840+ self.message_box.critical(EMAIL_MISMATCH)
2841 self.mocker.replay()
2842 self.assertFalse(self.controller.validate_form())
2843
2844@@ -334,17 +578,18 @@
2845 """Test the callback with a weak password."""
2846 weak_password = 'weak'
2847 email = 'email@example.com'
2848- self.view.wizard().app_name
2849- self.mocker.result(self.app_name)
2850+ captcha_solution = 'captcha'
2851 self.view.ui.email_edit.text()
2852 self.mocker.result(email)
2853 self.view.ui.confirm_email_edit.text()
2854 self.mocker.result(email)
2855 self.view.ui.password_edit.text()
2856 self.mocker.result(weak_password)
2857+ self.view.ui.captcha_solution_edit.text()
2858+ self.mocker.result(captcha_solution)
2859 self.view.ui.confirm_password_edit.text()
2860 self.mocker.result(weak_password)
2861- self.message_box.critical(self.view, self.app_name, PASSWORD_TOO_WEAK)
2862+ self.message_box.critical(PASSWORD_TOO_WEAK)
2863 self.mocker.replay()
2864 self.assertFalse(self.controller.validate_form())
2865
2866@@ -352,8 +597,7 @@
2867 """Test the callback where the password is wrong."""
2868 password = 'Qwerty9923'
2869 email = 'email@example.com'
2870- self.view.wizard().app_name
2871- self.mocker.result(self.app_name)
2872+ captcha_solution = 'captcha'
2873 self.view.ui.email_edit.text()
2874 self.mocker.result(email)
2875 self.view.ui.confirm_email_edit.text()
2876@@ -362,7 +606,28 @@
2877 self.mocker.result(password)
2878 self.view.ui.confirm_password_edit.text()
2879 self.mocker.result('other_password')
2880- self.message_box.critical(self.view, self.app_name, PASSWORD_MISMATCH)
2881+ self.view.ui.captcha_solution_edit.text()
2882+ self.mocker.result(captcha_solution)
2883+ self.message_box.critical(PASSWORD_MISMATCH)
2884+ self.mocker.replay()
2885+ self.assertFalse(self.controller.validate_form())
2886+
2887+ def test_set_next_validation_empty_captcha(self):
2888+ """Test the callback where the password is wrong."""
2889+ password = 'Qwerty9923'
2890+ email = 'email@example.com'
2891+ captcha_solution = ''
2892+ self.view.ui.email_edit.text()
2893+ self.mocker.result(email)
2894+ self.view.ui.confirm_email_edit.text()
2895+ self.mocker.result(email)
2896+ self.view.ui.password_edit.text()
2897+ self.mocker.result(password)
2898+ self.view.ui.confirm_password_edit.text()
2899+ self.mocker.result(password)
2900+ self.view.ui.captcha_solution_edit.text()
2901+ self.mocker.result(captcha_solution)
2902+ self.message_box.critical(CAPTCHA_REQUIRED_ERROR)
2903 self.mocker.replay()
2904 self.assertFalse(self.controller.validate_form())
2905
2906@@ -408,7 +673,7 @@
2907
2908 class FakeView(object):
2909
2910- """A fake view"""
2911+ """A fake view."""
2912
2913 app_name = "TestApp"
2914
2915@@ -417,12 +682,13 @@
2916 return self
2917
2918
2919-class SetupAccountControllerTest2(TestCase):
2920+class SetupAccountControllerValidationTest(TestCase):
2921
2922- """Tests for SetupAccountController, but without Mocker"""
2923+ """Tests for SetupAccountController, but without Mocker."""
2924
2925 def setUp(self):
2926- super(SetupAccountControllerTest2, self).setUp()
2927+ """Set the different tests."""
2928+ super(SetupAccountControllerValidationTest, self).setUp()
2929 self.message_box = FakeMessageBox()
2930 self.controller = SetUpAccountController(message_box=self.message_box)
2931 self.patch(self.controller, '_refresh_captcha', self._set_called)
2932@@ -443,9 +709,7 @@
2933 self.controller.on_user_registration_error('TestApp',
2934 {'__all__': "Error in All"})
2935 self.assertEqual(self.message_box.critical_args, ((
2936- self.controller.view,
2937- self.controller.view.app_name,
2938- "Error in All"), {}))
2939+ "Error in All", ), {}))
2940
2941 def test_on_user_registration_all_fields(self):
2942 """Pass all known error keys, plus unknown one."""
2943@@ -456,11 +720,7 @@
2944 'unknownfield': "Error in unknown",
2945 })
2946 self.assertEqual(self.message_box.critical_args, ((
2947- self.controller.view,
2948- self.controller.view.app_name,
2949- "Error in All\nError in email\n"
2950- "Error in password\nError in unknown",
2951- ), {}))
2952+ "Error in All", ), {}))
2953
2954
2955 class TosControllerTestCase(MockerTestCase):
2956@@ -481,8 +741,8 @@
2957
2958 def test_setup_ui(self):
2959 """Test the set up of the ui."""
2960- self.view.setTitle(self.title)
2961- self.view.setSubTitle(self.subtitle)
2962+ self.view.header.set_title(self.title)
2963+ self.view.header.set_subtitle(self.subtitle)
2964 self.view.ui.terms_webkit.load(ANY)
2965 self.view.ui.tos_link_label.setText(
2966 TOS_LABEL %
2967@@ -499,7 +759,8 @@
2968 super(EmailVerificationControllerTestCase, self).setUp()
2969 self.view = self.mocker.mock()
2970 self.backend = self.mocker.mock()
2971- self.controller = EmailVerificationController()
2972+ self.controller = EmailVerificationController(
2973+ message_box=self.mocker.mock())
2974 self.controller.view = self.view
2975 self.controller.backend = self.backend
2976
2977@@ -521,15 +782,17 @@
2978
2979 def test_set_titles(self):
2980 """Test that the titles are set."""
2981- self.view.setTitle(VERIFY_EMAIL_TITLE)
2982- self.view.wizard().app_name
2983- self.view.wizard().field("email_address").toString()
2984- self.view.setSubTitle(VERIFY_EMAIL_CONTENT % {
2985+ self.view.header.set_title(VERIFY_EMAIL_CONTENT)
2986+ self.view.header.set_subtitle(VERIFY_EMAIL_CONTENT % {
2987 "app_name": None,
2988 "email": None,
2989 })
2990 self.mocker.replay()
2991- self.controller._set_titles()
2992+ self.view.header.set_title(VERIFY_EMAIL_CONTENT)
2993+ self.view.header.set_subtitle(VERIFY_EMAIL_CONTENT % {
2994+ "app_name": None,
2995+ "email": None,
2996+ })
2997
2998 def test_validate_email(self):
2999 """Test the callback."""
3000@@ -550,6 +813,27 @@
3001 self.controller.validate_email()
3002
3003
3004+class EmailVerificationControllerValidationTestCase(TestCase):
3005+ """Tests for EmailVerificationController, but without Mocker."""
3006+
3007+ def setUp(self):
3008+ """Set the different tests."""
3009+ super(EmailVerificationControllerValidationTestCase, self).setUp()
3010+ self.message_box = FakeMessageBox()
3011+ self.controller = EmailVerificationController(
3012+ message_box=self.message_box)
3013+ self.patch(self.controller, 'view', FakeView())
3014+ self._called = False
3015+
3016+ def test_on_email_validation_error(self):
3017+ """Test that on_email_validation_error callback works as expected."""
3018+ error = dict(errtype='BadTokenError')
3019+ app_name = 'app_name'
3020+ self.controller.on_email_validation_error(app_name, error)
3021+ self.assertEqual(self.message_box.critical_args,
3022+ (("Error: {'errtype': 'BadTokenError'}",), {}))
3023+
3024+
3025 class ErrorControllerTestCase(MockerTestCase):
3026 """Test the success page controller."""
3027
3028@@ -564,10 +848,21 @@
3029
3030 def test_set_ui(self):
3031 """Test the process that sets the ui."""
3032- self.view.next = -1
3033- self.view.ui.error_message_label
3034- self.mocker.result(self.view)
3035- self.view.setText(ERROR)
3036+ self.controller._title = ERROR
3037+ self.controller._subtitle = ERROR
3038+ self.view.next = -1
3039+ self.view.ui.error_message_label.setText(ERROR)
3040+ self.view.header.set_title(ERROR)
3041+ self.view.header.set_subtitle(ERROR)
3042+ self.mocker.replay()
3043+ self.controller.setupUi(self.view)
3044+
3045+ def test_hide_titles(self):
3046+ """Test how the different titles are set."""
3047+ self.view.next = -1
3048+ self.view.ui.error_message_label.setText(ERROR)
3049+ self.view.header.set_title('')
3050+ self.view.header.set_subtitle('')
3051 self.mocker.replay()
3052 self.controller.setupUi(self.view)
3053
3054@@ -586,10 +881,21 @@
3055
3056 def test_set_ui(self):
3057 """Test the process that sets the ui."""
3058- self.view.next = -1
3059- self.view.ui.success_message_label
3060- self.mocker.result(self.view)
3061- self.view.setText(SUCCESS)
3062+ self.controller._title = SUCCESS
3063+ self.controller._subtitle = SUCCESS
3064+ self.view.next = -1
3065+ self.view.ui.success_message_label.setText(SUCCESS)
3066+ self.view.header.set_title(SUCCESS)
3067+ self.view.header.set_subtitle(SUCCESS)
3068+ self.mocker.replay()
3069+ self.controller.setupUi(self.view)
3070+
3071+ def test_hide_titles(self):
3072+ """Test how the different titles are set."""
3073+ self.view.next = -1
3074+ self.view.ui.success_message_label.setText(SUCCESS)
3075+ self.view.header.set_title('')
3076+ self.view.header.set_subtitle('')
3077 self.mocker.replay()
3078 self.controller.setupUi(self.view)
3079
3080@@ -683,6 +989,75 @@
3081 self.controller.setupUi(self.view)
3082
3083
3084+class FakeForgottenPasswordPage(FakeView):
3085+ """Fake the page."""
3086+
3087+ def __init__(self):
3088+ self.email_address_line_edit = self
3089+ self.send_button = self
3090+ self._text = ""
3091+ self._enabled = False
3092+ super(FakeForgottenPasswordPage, self).__init__()
3093+
3094+ def text(self):
3095+ """Return text."""
3096+ return self._text
3097+
3098+ # setText, setEnabled are inherited
3099+ # pylint: disable=C0103
3100+ def setText(self, text):
3101+ """Save text."""
3102+ self._text = text
3103+
3104+ def setEnabled(self, value):
3105+ """Fake setEnabled."""
3106+ self._enabled = value
3107+
3108+ def enabled(self):
3109+ """Fake enabled."""
3110+ return self._enabled
3111+
3112+
3113+class ForgottenPasswordControllerValidationTest(TestCase):
3114+
3115+ """Tests for ForgottenPasswordController, but without Mocker."""
3116+
3117+ def setUp(self):
3118+ """Set the different tests."""
3119+ super(ForgottenPasswordControllerValidationTest, self).setUp()
3120+ self.message_box = FakeMessageBox()
3121+ self.controller = ForgottenPasswordController(
3122+ message_box=self.message_box)
3123+ self.controller.view = FakeForgottenPasswordPage()
3124+ self._called = False
3125+
3126+ def _set_called(self, *args, **kwargs):
3127+ """Store 'args' and 'kwargs' for test assertions."""
3128+ self._called = (args, kwargs)
3129+
3130+ def test_valid(self):
3131+ """The submit button should be enabled with a valid email."""
3132+ self.controller.view.email_address_line_edit.setText("a@b")
3133+ self.assertNotEqual(unicode(
3134+ self.controller.view.email_address_line_edit.text()), u"")
3135+ self.controller._validate()
3136+ self.assertTrue(self.controller.view.send_button.enabled())
3137+
3138+ def test_invalid(self):
3139+ """The submit button should be disabled with an invalid email."""
3140+ self.controller.view.email_address_line_edit.setText("ab")
3141+ self.assertNotEqual(
3142+ unicode(self.controller.view.email_address_line_edit.text()), u"")
3143+ self.controller._validate()
3144+ self.assertFalse(self.controller.view.send_button.enabled())
3145+
3146+ def test_empty(self):
3147+ """The submit button should be disabled without email."""
3148+ self.assertEqual(
3149+ unicode(self.controller.view.email_address_line_edit.text()), u"")
3150+ self.assertFalse(self.controller.view.send_button.enabled())
3151+
3152+
3153 class ForgottenPasswordControllerTestCase(MockerTestCase):
3154 """Test the controller of the fogotten password page."""
3155
3156@@ -729,6 +1104,7 @@
3157
3158 def test_connect_ui(self):
3159 """Test that the correct ui signals are connected."""
3160+ self.view.email_address_line_edit.textChanged.connect(MATCH(callable))
3161 self.view.send_button.clicked.connect(MATCH(callable))
3162 self.view.try_again_button.clicked.connect(
3163 self.controller.on_try_again)
3164@@ -789,6 +1165,7 @@
3165
3166 def test_set_translated_strings(self):
3167 """Ensure that the correct strings are set."""
3168+ self.controller._subtitle = PASSWORD_HELP
3169 self.view.ui.reset_code_line_edit.setPlaceholderText(RESET_CODE_ENTRY)
3170 self.view.ui.password_line_edit.setPlaceholderText(PASSWORD1_ENTRY)
3171 self.view.ui.confirm_password_line_edit.setPlaceholderText(
3172@@ -806,6 +1183,13 @@
3173 self.controller.on_password_changed
3174 self.backend.on_password_change_error_cb = \
3175 self.controller.on_password_change_error
3176+ self.view.ui.reset_code_line_edit.textChanged.connect(
3177+ MATCH(callable))
3178+ self.view.ui.password_line_edit.textChanged.connect(
3179+ MATCH(callable))
3180+ self.view.ui.confirm_password_line_edit.textChanged.connect(
3181+ MATCH(callable))
3182+
3183 self.mocker.replay()
3184 self.controller._connect_ui()
3185
3186@@ -861,3 +1245,75 @@
3187 self.mocker.replay()
3188 self.assertFalse(self.controller.is_correct_password_confirmation(
3189 password))
3190+
3191+
3192+class FakeResetPasswordPage(object):
3193+
3194+ """Fake ResetPasswordPage."""
3195+
3196+ def __init__(self, *args):
3197+ """Initialize."""
3198+ self.ui = self
3199+ self.ui.reset_code_line_edit = FakeLineEdit()
3200+ self.ui.password_line_edit = FakeLineEdit()
3201+ self.ui.confirm_password_line_edit = FakeLineEdit()
3202+ self.reset_password_button = self
3203+ self._enabled = 9 # Intentionally wrong.
3204+
3205+ # setEnabled is inherited
3206+ # pylint: disable=C0103
3207+ def setEnabled(self, value):
3208+ """Fake setEnabled."""
3209+ self._enabled = value
3210+
3211+ def enabled(self):
3212+ """Fake enabled."""
3213+ return self._enabled
3214+
3215+
3216+class ResetPasswordControllerValidationTest(TestCase):
3217+
3218+ """Tests for ResetPasswordController, but without Mocker."""
3219+
3220+ def setUp(self):
3221+ """Setup test."""
3222+ super(ResetPasswordControllerValidationTest, self).setUp()
3223+ self.controller = ResetPasswordController()
3224+ self.controller.view = FakeResetPasswordPage()
3225+ self._called = False
3226+
3227+ def _set_called(self, *args, **kwargs):
3228+ """Store 'args' and 'kwargs' for test assertions."""
3229+ self._called = (args, kwargs)
3230+
3231+ def test_valid(self):
3232+ """Enable the button with valid data."""
3233+ self.controller.view.reset_code_line_edit.setText("ABCD")
3234+ self.controller.view.password_line_edit.setText("1234567A")
3235+ self.controller.view.confirm_password_line_edit.setText("1234567A")
3236+ self.controller._validate()
3237+ self.assertTrue(self.controller.view.reset_password_button.enabled())
3238+
3239+ def test_invalid_code(self):
3240+ """Disable the button with an invalid code."""
3241+ self.controller.view.reset_code_line_edit.setText("")
3242+ self.controller.view.password_line_edit.setText("1234567A")
3243+ self.controller.view.confirm_password_line_edit.setText("1234567A")
3244+ self.controller._validate()
3245+ self.assertFalse(self.controller.view.reset_password_button.enabled())
3246+
3247+ def test_invalid_password(self):
3248+ """Disable the button with an invalid password."""
3249+ self.controller.view.reset_code_line_edit.setText("")
3250+ self.controller.view.password_line_edit.setText("1234567")
3251+ self.controller.view.confirm_password_line_edit.setText("1234567")
3252+ self.controller._validate()
3253+ self.assertFalse(self.controller.view.reset_password_button.enabled())
3254+
3255+ def test_invalid_confirm(self):
3256+ """Disable the button with an invalid password confirm."""
3257+ self.controller.view.reset_code_line_edit.setText("")
3258+ self.controller.view.password_line_edit.setText("1234567A")
3259+ self.controller.view.confirm_password_line_edit.setText("1234567")
3260+ self.controller._validate()
3261+ self.assertFalse(self.controller.view.reset_password_button.enabled())
3262
3263=== modified file 'ubuntu_sso/tests/__init__.py'
3264--- ubuntu_sso/tests/__init__.py 2011-01-12 18:56:56 +0000
3265+++ ubuntu_sso/tests/__init__.py 2011-08-25 19:16:19 +0000
3266@@ -22,30 +22,30 @@
3267
3268 from ubuntu_sso.keyring import get_token_name
3269
3270-APP_NAME = 'The Super App!'
3271-CAPTCHA_ID = 'test'
3272+APP_NAME = u'The Super App!'
3273+CAPTCHA_ID = u'test'
3274 CAPTCHA_PATH = os.path.abspath(os.path.join(os.curdir, 'ubuntu_sso', 'tests',
3275 'files', 'captcha.png'))
3276-CAPTCHA_SOLUTION = 'william Byrd'
3277-EMAIL = 'test@example.com'
3278-EMAIL_TOKEN = 'B2Pgtf'
3279+CAPTCHA_SOLUTION = u'william Byrd'
3280+EMAIL = u'test@example.com'
3281+EMAIL_TOKEN = u'B2Pgtf'
3282 GTK_GUI_CLASS = 'UbuntuSSOClientGUI'
3283 GTK_GUI_MODULE = 'ubuntu_sso.gtk.gui'
3284 HELP_TEXT = """Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam sed
3285 lorem nibh. Suspendisse gravida nulla non nunc suscipit pulvinar tempus ut
3286 augue. Morbi consequat, ligula a elementum pretium, dolor nulla tempus metus,
3287 sed viverra nisi risus non velit."""
3288-NAME = 'Juanito Pérez'
3289-PASSWORD = 'h3lloWorld'
3290-PING_URL = 'http://localhost/ping-me/'
3291-RESET_PASSWORD_TOKEN = '8G5Wtq'
3292+NAME = u'Juanito Pérez'
3293+PASSWORD = u'h3lloWorld'
3294+PING_URL = u'http://localhost/ping-me/'
3295+RESET_PASSWORD_TOKEN = u'8G5Wtq'
3296 TOKEN = {u'consumer_key': u'xQ7xDAz',
3297 u'consumer_secret': u'KzCJWCTNbbntwfyCKKjomJDzlgqxLy',
3298 u'token_name': u'test',
3299 u'token': u'GkInOfSMGwTXAUoVQwLUoPxElEEUdhsLVNTPhxHJDUIeHCPNEo',
3300 u'token_secret': u'qFYImEtlczPbsCnYyuwLoPDlPEnvNcIktZphPQklAWrvyfFMV'}
3301 TOKEN_NAME = get_token_name(APP_NAME)
3302-TC_URL = 'http://localhost/'
3303+TC_URL = u'http://localhost/'
3304 WINDOW_ID = 5
3305
3306
3307
3308=== modified file 'ubuntu_sso/tests/test_account.py'
3309--- ubuntu_sso/tests/test_account.py 2011-07-22 20:12:20 +0000
3310+++ ubuntu_sso/tests/test_account.py 2011-08-25 19:16:19 +0000
3311@@ -50,9 +50,11 @@
3312 class FakedCaptchas(object):
3313 """Fake the captcha generator."""
3314
3315+ image_uri = 'file://localhost/%s' % CAPTCHA_PATH.replace(os.path.sep, '/')
3316+
3317 def new(self):
3318 """Return a local fake captcha."""
3319- return {'image_url': 'file://%s' % CAPTCHA_PATH,
3320+ return {'image_url': self.image_uri,
3321 'captcha_id': CAPTCHA_ID}
3322
3323
3324@@ -162,7 +164,8 @@
3325 def test_generate_captcha(self):
3326 """Captcha can be generated."""
3327 filename = self.mktemp()
3328- self.addCleanup(lambda: os.remove(filename))
3329+ self.addCleanup(lambda: os.remove(filename)
3330+ if os.path.exists(filename) else None)
3331 captcha_id = self.processor.generate_captcha(filename)
3332 self.assertEqual(CAPTCHA_ID, captcha_id, 'captcha id must be correct.')
3333 self.assertTrue(os.path.isfile(filename), '%s must exist.' % filename)
3334
3335=== modified file 'ubuntu_sso/tests/test_credentials.py'
3336--- ubuntu_sso/tests/test_credentials.py 2011-06-24 19:14:17 +0000
3337+++ ubuntu_sso/tests/test_credentials.py 2011-08-25 19:16:19 +0000
3338@@ -20,19 +20,19 @@
3339
3340 import logging
3341 import os
3342-import urllib
3343+import urllib2
3344
3345 from twisted.internet import defer
3346-from twisted.internet.defer import inlineCallbacks
3347 from twisted.trial.unittest import TestCase, FailTest
3348 from ubuntuone.devtools.handlers import MementoHandler
3349
3350 from ubuntu_sso import credentials
3351+import ubuntu_sso.main
3352 from ubuntu_sso.credentials import (APP_NAME_KEY, HELP_TEXT_KEY, NO_OP,
3353 PING_URL_KEY, TC_URL_KEY, UI_CLASS_KEY, UI_MODULE_KEY, WINDOW_ID_KEY,
3354 ERROR_KEY, ERROR_DETAIL_KEY)
3355 from ubuntu_sso.tests import (APP_NAME, EMAIL, GTK_GUI_CLASS, GTK_GUI_MODULE,
3356- HELP_TEXT, PING_URL, TC_URL, TOKEN, WINDOW_ID)
3357+ HELP_TEXT, PASSWORD, PING_URL, TC_URL, TOKEN, WINDOW_ID)
3358
3359
3360 # Access to a protected member of a client class
3361@@ -41,6 +41,9 @@
3362 # Attribute defined outside __init__
3363 # pylint: disable=W0201
3364
3365+# Instance of 'class' has no 'x' member (but some types could not be inferred)
3366+# pylint: disable=E1103
3367+
3368
3369 KWARGS = {
3370 APP_NAME_KEY: APP_NAME,
3371@@ -64,11 +67,13 @@
3372 """An error to be used while testing."""
3373
3374
3375-class FailingClientGUI(object):
3376- """Fake a failing SSO GUI."""
3377+class FailingClient(object):
3378+ """Fake a failing client."""
3379+
3380+ err_msg = 'A failing class.'
3381
3382 def __init__(self, *args, **kwargs):
3383- raise SampleMiscException('A failing GUI class.')
3384+ raise SampleMiscException(self.err_msg)
3385
3386
3387 class FakedClientGUI(object):
3388@@ -82,6 +87,18 @@
3389 self.user_cancellation_callback = None
3390
3391
3392+class FakedSSOLoginRoot(object):
3393+ """Fake a SSOLoginRoot."""
3394+
3395+ args = []
3396+ kwargs = {}
3397+
3398+ def login(self, *args, **kwargs):
3399+ """Fake login."""
3400+ self.args = args
3401+ self.kwargs = kwargs
3402+
3403+
3404 class BasicTestCase(TestCase):
3405 """Test case with a helper tracker."""
3406
3407@@ -113,10 +130,6 @@
3408 denial_cb=self.denial,
3409 **KWARGS)
3410
3411- def tearDown(self):
3412- """Clean up."""
3413- super(CredentialsTestCase, self).tearDown()
3414-
3415 def success(self, *args, **kwargs):
3416 """To be called on success."""
3417 self._set_called('success', *args, **kwargs)
3418@@ -232,7 +245,7 @@
3419 class CredentialsLoginSuccessTestCase(CredentialsTestCase):
3420 """Test suite for the Credentials login success callback."""
3421
3422- @inlineCallbacks
3423+ @defer.inlineCallbacks
3424 def test_cred_not_found(self):
3425 """On auth success, if cred not found, self.error_cb is called."""
3426 self.patch(credentials.Keyring, 'get_credentials',
3427@@ -245,7 +258,7 @@
3428 detailed_error=AssertionError(msg))
3429 self.assertEqual(result, None)
3430
3431- @inlineCallbacks
3432+ @defer.inlineCallbacks
3433 def test_cred_error(self):
3434 """On auth success, if cred failed, self.error_cb is called."""
3435 expected_error = SampleMiscException()
3436@@ -259,7 +272,7 @@
3437 self.assertEqual(result, None)
3438 self.assertTrue(self.memento.check_exception(SampleMiscException))
3439
3440- @inlineCallbacks
3441+ @defer.inlineCallbacks
3442 def test_ping_success(self):
3443 """Auth success + cred found + ping success, success_cb is called."""
3444 self.patch(credentials.Keyring, 'get_credentials',
3445@@ -271,7 +284,7 @@
3446 self.assertEqual(self._called, (('success', APP_NAME, TOKEN), {}))
3447 self.assertEqual(result, 0)
3448
3449- @inlineCallbacks
3450+ @defer.inlineCallbacks
3451 def test_ping_error(self):
3452 """Auth success + cred found + ping error, error_cb is called.
3453
3454@@ -299,7 +312,7 @@
3455 # exception logged
3456 self.assertTrue(self.memento.check_exception(FailTest, error))
3457
3458- @inlineCallbacks
3459+ @defer.inlineCallbacks
3460 def test_pings_url(self):
3461 """On auth success, self.ping_url is opened."""
3462 self.patch(credentials.Keyring, 'get_credentials',
3463@@ -315,7 +328,7 @@
3464
3465 self.assertEqual(self._url_pinged, ((APP_NAME, EMAIL, TOKEN), {}))
3466
3467- @inlineCallbacks
3468+ @defer.inlineCallbacks
3469 def test_no_ping_url_is_success(self):
3470 """Auth success + cred found + no ping url, success_cb is called.
3471
3472@@ -353,15 +366,15 @@
3473 def faked_urlopen(request):
3474 """Fake urlopener."""
3475 self._request = request
3476- response = urllib.addinfourl(fp=open(os.path.devnull),
3477- headers=request.headers,
3478- url=request.get_full_url(),
3479- code=200)
3480+ response = urllib2.addinfourl(fp=open(os.path.devnull),
3481+ headers=request.headers,
3482+ url=request.get_full_url(),
3483+ code=200)
3484 return response
3485
3486 self.patch(credentials.urllib2, 'urlopen', faked_urlopen)
3487
3488- @inlineCallbacks
3489+ @defer.inlineCallbacks
3490 def test_ping_url_if_url_is_none(self):
3491 """self.ping_url is opened."""
3492 self.patch(credentials.urllib2, 'urlopen', self.fail)
3493@@ -370,7 +383,7 @@
3494 credentials=TOKEN)
3495 # no failure
3496
3497- @inlineCallbacks
3498+ @defer.inlineCallbacks
3499 def test_ping_url(self):
3500 """On auth success, self.ping_url is opened."""
3501 yield self.obj._ping_url(app_name=APP_NAME, email=EMAIL,
3502@@ -380,23 +393,23 @@
3503 self.assertEqual(self._request.get_full_url(),
3504 self.obj.ping_url + EMAIL)
3505
3506- @inlineCallbacks
3507+ @defer.inlineCallbacks
3508 def test_request_is_signed_with_credentials(self):
3509 """The http request to self.ping_url is signed with the credentials."""
3510+
3511+ def fake_it(*a, **kw):
3512+ """Fake oauth_headers."""
3513+ self._set_called(*a, **kw)
3514+ return {}
3515+
3516+ self.patch(credentials.utils, 'oauth_headers', fake_it)
3517 result = yield self.obj._ping_url(APP_NAME, EMAIL, TOKEN)
3518- headers = self._request.headers
3519-
3520- self.assertIn('Authorization', headers)
3521- oauth_stuff = headers['Authorization']
3522-
3523- expected = 'oauth_consumer_key="xQ7xDAz", ' \
3524- 'oauth_signature_method="HMAC-SHA1", oauth_version="1.0", ' \
3525- 'oauth_token="GkInOfSMGwTXAUoVQwLUoPxElEEUdhsLVNTPhxHJDUIeHCPNEo' \
3526- '", oauth_signature="'
3527- self.assertIn(expected, oauth_stuff)
3528- self.assertEqual(result, 200)
3529-
3530- @inlineCallbacks
3531+
3532+ self.assertEqual(self._called,
3533+ ((self.obj.ping_url + EMAIL, TOKEN), {}))
3534+ self.assertEqual(result.code, 200)
3535+
3536+ @defer.inlineCallbacks
3537 def test_ping_url_error(self):
3538 """Exception is handled if ping fails."""
3539 error = 'Blu'
3540@@ -408,12 +421,52 @@
3541 self.assert_error_cb_called(msg=msg, detailed_error=FailTest(error))
3542 self.assertTrue(self.memento.check_exception(FailTest, error))
3543
3544+ @defer.inlineCallbacks
3545+ def test_ping_url_formatting(self):
3546+ """The email is added as the first formatting argument."""
3547+ self.obj.ping_url = u'http://example.com/{email}/something/else'
3548+ result = yield self.obj._ping_url(app_name=APP_NAME, email=EMAIL,
3549+ credentials=TOKEN)
3550+
3551+ expected = self.obj.ping_url.format(email=EMAIL)
3552+ self.assertEqual(expected, result.url)
3553+
3554+ @defer.inlineCallbacks
3555+ def test_ping_url_formatting_with_query_params(self):
3556+ """The email is added as the first formatting argument."""
3557+ self.obj.ping_url = u'http://example.com/{email}?something=else'
3558+ result = yield self.obj._ping_url(app_name=APP_NAME, email=EMAIL,
3559+ credentials=TOKEN)
3560+
3561+ expected = self.obj.ping_url.format(email=EMAIL)
3562+ self.assertEqual(expected, result.url)
3563+
3564+ @defer.inlineCallbacks
3565+ def test_ping_url_formatting_no_email_kwarg(self):
3566+ """The email is added as the first formatting argument."""
3567+ self.obj.ping_url = u'http://example.com/{0}/yadda/?something=else'
3568+ result = yield self.obj._ping_url(app_name=APP_NAME, email=EMAIL,
3569+ credentials=TOKEN)
3570+
3571+ expected = self.obj.ping_url.format(EMAIL)
3572+ self.assertEqual(expected, result.url)
3573+
3574+ @defer.inlineCallbacks
3575+ def test_ping_url_formatting_no_format(self):
3576+ """The email is appended if formatting could not be accomplished."""
3577+ self.obj.ping_url = u'http://example.com/yadda/'
3578+ result = yield self.obj._ping_url(app_name=APP_NAME, email=EMAIL,
3579+ credentials=TOKEN)
3580+
3581+ expected = self.obj.ping_url + EMAIL
3582+ self.assertEqual(expected, result.url)
3583+
3584
3585 class FindCredentialsTestCase(CredentialsTestCase):
3586 """Test suite for the find_credentials method."""
3587 timeout = 5
3588
3589- @inlineCallbacks
3590+ @defer.inlineCallbacks
3591 def test_find_credentials(self):
3592 """A deferred with credentials is returned when found."""
3593 self.patch(credentials.Keyring, 'get_credentials',
3594@@ -422,7 +475,7 @@
3595 token = yield self.obj.find_credentials()
3596 self.assertEqual(token, TOKEN)
3597
3598- @inlineCallbacks
3599+ @defer.inlineCallbacks
3600 def test_credentials_not_found(self):
3601 """find_credentials returns {} when no credentials are found."""
3602 self.patch(credentials.Keyring, 'get_credentials',
3603@@ -431,7 +484,7 @@
3604 token = yield self.obj.find_credentials()
3605 self.assertEqual(token, {})
3606
3607- @inlineCallbacks
3608+ @defer.inlineCallbacks
3609 def test_keyring_failure(self):
3610 """Failures from the keyring are handled."""
3611 expected_error = SampleMiscException()
3612@@ -445,7 +498,7 @@
3613 class ClearCredentialsTestCase(CredentialsTestCase):
3614 """Test suite for the clear_credentials method."""
3615
3616- @inlineCallbacks
3617+ @defer.inlineCallbacks
3618 def test_clear_credentials(self):
3619 """The credentials are cleared."""
3620 self.patch(credentials.Keyring, 'delete_credentials',
3621@@ -454,7 +507,7 @@
3622 yield self.obj.clear_credentials()
3623 self.assertEqual(self._called, ((APP_NAME,), {}))
3624
3625- @inlineCallbacks
3626+ @defer.inlineCallbacks
3627 def test_keyring_failure(self):
3628 """Failures from the keyring are handled."""
3629 expected_error = SampleMiscException()
3630@@ -468,7 +521,7 @@
3631 class StoreCredentialsTestCase(CredentialsTestCase):
3632 """Test suite for the store_credentials method."""
3633
3634- @inlineCallbacks
3635+ @defer.inlineCallbacks
3636 def test_store_credentials(self):
3637 """The credentials are stored."""
3638 self.patch(credentials.Keyring, 'set_credentials',
3639@@ -477,7 +530,7 @@
3640 yield self.obj.store_credentials(TOKEN)
3641 self.assertEqual(self._called, ((APP_NAME, TOKEN,), {}))
3642
3643- @inlineCallbacks
3644+ @defer.inlineCallbacks
3645 def test_keyring_failure(self):
3646 """Failures from the keyring are handled."""
3647 expected_error = SampleMiscException()
3648@@ -493,85 +546,89 @@
3649
3650 operation = 'register'
3651 login_only = False
3652+ kwargs = {}
3653+ inner_class = FakedClientGUI
3654
3655 def setUp(self):
3656 super(RegisterTestCase, self).setUp()
3657- self.ui_kwargs = UI_KWARGS.copy()
3658- self.ui_kwargs['login_only'] = self.login_only
3659+ self.inner_kwargs = UI_KWARGS.copy()
3660+ self.inner_kwargs['login_only'] = self.login_only
3661+ self.method_call = getattr(self.obj, self.operation)
3662
3663- @inlineCallbacks
3664+ @defer.inlineCallbacks
3665 def test_with_existent_token(self):
3666 """The operation returns the credentials if already in keyring."""
3667 self.patch(credentials.Keyring, 'get_credentials',
3668 lambda kr, app: defer.succeed(TOKEN))
3669
3670- yield getattr(self.obj, self.operation)()
3671+ yield self.method_call(**self.kwargs)
3672
3673 self.assertEqual(self._called, (('success', APP_NAME, TOKEN), {}))
3674
3675- @inlineCallbacks
3676+ @defer.inlineCallbacks
3677 def test_without_existent_token(self):
3678- """The operation returns the credentials gathered by the GUI."""
3679+ """The operation returns the credentials gathered by the inner call."""
3680 self.patch(credentials.Keyring, 'get_credentials',
3681 lambda kr, app: defer.succeed(None))
3682
3683- yield getattr(self.obj, self.operation)()
3684-
3685- self.assertEqual(self.obj.gui.kwargs, self.ui_kwargs)
3686-
3687- @inlineCallbacks
3688+ yield self.method_call(**self.kwargs)
3689+
3690+ self.assertEqual(self.obj.inner.kwargs, self.inner_kwargs)
3691+
3692+ @defer.inlineCallbacks
3693 def test_with_exception_on_credentials(self):
3694 """The operation calls the error callback if a exception occurs."""
3695 expected_error = SampleMiscException()
3696 self.patch(credentials.Keyring, 'get_credentials',
3697 lambda kr, app: defer.fail(expected_error))
3698
3699- yield getattr(self.obj, self.operation)()
3700+ yield self.method_call(**self.kwargs)
3701
3702 msg = 'Problem while retrieving credentials'
3703 self.assert_error_cb_called(msg=msg, detailed_error=expected_error)
3704 self.assertTrue(self.memento.check_exception(SampleMiscException))
3705
3706- @inlineCallbacks
3707- def test_with_exception_on_gui(self):
3708- """The operation calls the error callback if a GUI exception occurs."""
3709+ @defer.inlineCallbacks
3710+ def test_with_exception_on_inner_call(self, msg=None):
3711+ """The operation calls the error callback if a exception occurs."""
3712 self.patch(credentials.Keyring, 'get_credentials',
3713 lambda kr, app: defer.succeed(None))
3714- err = 'A failing GUI class.'
3715- self.obj.ui_class = 'FailingClientGUI'
3716-
3717- yield getattr(self.obj, self.operation)()
3718-
3719- msg = 'Problem opening the Ubuntu SSO user interface'
3720+ self.obj.ui_class = 'FailingClient'
3721+
3722+ yield self.method_call(**self.kwargs)
3723+
3724+ if msg is None:
3725+ msg = 'Problem opening the Ubuntu SSO user interface'
3726 self.assert_error_cb_called(msg=msg,
3727- detailed_error=SampleMiscException(err))
3728- self.assertTrue(self.memento.check_exception(SampleMiscException, err))
3729+ detailed_error=SampleMiscException(FailingClient.err_msg))
3730+ self.assertTrue(self.memento.check_exception(SampleMiscException,
3731+ FailingClient.err_msg))
3732
3733- @inlineCallbacks
3734- def test_connects_gui_signals(self):
3735- """GUI callbacks are properly connected."""
3736+ @defer.inlineCallbacks
3737+ def test_connects_inner_signals(self):
3738+ """Inner callbacks are properly connected."""
3739 self.patch(credentials.Keyring, 'get_credentials',
3740 lambda kr, app: defer.succeed(None))
3741- yield getattr(self.obj, self.operation)()
3742+ yield self.method_call(**self.kwargs)
3743
3744- self.assertEqual(self.obj.gui.login_success_callback,
3745- self.obj._login_success_cb)
3746- self.assertEqual(self.obj.gui.registration_success_callback,
3747- self.obj._login_success_cb)
3748- self.assertEqual(self.obj.gui.user_cancellation_callback,
3749+ self.assertEqual(self.obj.inner.login_success_callback,
3750+ self.obj._login_success_cb)
3751+ self.assertEqual(self.obj.inner.registration_success_callback,
3752+ self.obj._login_success_cb)
3753+ self.assertEqual(self.obj.inner.user_cancellation_callback,
3754 self.obj._auth_denial_cb)
3755
3756- @inlineCallbacks
3757- def test_gui_is_created(self):
3758- """The GUI is created and stored."""
3759+ @defer.inlineCallbacks
3760+ def test_inner_object_is_created(self):
3761+ """The inner object is created and stored."""
3762 self.patch(credentials.Keyring, 'get_credentials',
3763 lambda kr, app: defer.succeed(None))
3764
3765- yield getattr(self.obj, self.operation)()
3766+ yield self.method_call(**self.kwargs)
3767
3768- self.assertIsInstance(self.obj.gui, FakedClientGUI)
3769- self.assertEqual(self.obj.gui.args, ())
3770- self.assertEqual(self.obj.gui.kwargs, self.ui_kwargs)
3771+ self.assertIsInstance(self.obj.inner, self.inner_class)
3772+ self.assertEqual(self.obj.inner.args, ())
3773+ self.assertEqual(self.obj.inner.kwargs, self.inner_kwargs)
3774
3775
3776 class LoginTestCase(RegisterTestCase):
3777@@ -579,3 +636,32 @@
3778
3779 operation = 'login'
3780 login_only = True
3781+
3782+
3783+class LoginEmailPasswordTestCase(RegisterTestCase):
3784+ """Test suite for the login_email_password method."""
3785+
3786+ operation = 'login_email_password'
3787+ login_only = True
3788+ kwargs = {'email': EMAIL, 'password': PASSWORD}
3789+ inner_class = FakedSSOLoginRoot
3790+
3791+ def setUp(self):
3792+ super(LoginEmailPasswordTestCase, self).setUp()
3793+ self.inner_kwargs = {APP_NAME_KEY: APP_NAME, 'email': EMAIL,
3794+ 'password': PASSWORD,
3795+ 'result_cb': self.obj._login_success_cb,
3796+ 'error_cb': self.obj._error_cb,
3797+ 'not_validated_cb': self.obj._error_cb}
3798+ self.patch(ubuntu_sso.main, 'SSOLoginRoot', FakedSSOLoginRoot)
3799+
3800+ def test_with_exception_on_inner_call(self, msg=None):
3801+ """The operation calls the error callback if a exception occurs."""
3802+ self.patch(ubuntu_sso.main, 'SSOLoginRoot', FailingClient)
3803+ msg = 'Problem logging with email and password.'
3804+ return super(LoginEmailPasswordTestCase,
3805+ self).test_with_exception_on_inner_call(msg=msg)
3806+
3807+ def test_connects_inner_signals(self):
3808+ """Inner callbacks are properly connected."""
3809+ # there is no inner callbacks for the SSOLoginRoot object
3810
3811=== modified file 'ubuntu_sso/utils/__init__.py'
3812--- ubuntu_sso/utils/__init__.py 2010-11-30 13:21:17 +0000
3813+++ ubuntu_sso/utils/__init__.py 2011-08-25 19:16:19 +0000
3814@@ -17,3 +17,38 @@
3815 # with this program. If not, see <http://www.gnu.org/licenses/>.
3816
3817 """Utility modules that may find use outside ubuntu_sso."""
3818+
3819+import cgi
3820+
3821+from oauth import oauth
3822+from urlparse import urlparse
3823+
3824+
3825+def oauth_headers(url, credentials, http_method='GET'):
3826+ """Sign 'url' using 'credentials'.
3827+
3828+ * 'url' must be a valid unicode url.
3829+ * 'credentials' must be a valid OAuth token.
3830+
3831+ Return oauth headers that can be pass to any Request like object.
3832+
3833+ """
3834+ assert isinstance(url, unicode)
3835+ url = url.encode('utf-8')
3836+ _, _, _, _, query, _ = urlparse(url)
3837+ parameters = dict(cgi.parse_qsl(query))
3838+
3839+ consumer = oauth.OAuthConsumer(credentials['consumer_key'],
3840+ credentials['consumer_secret'])
3841+ token = oauth.OAuthToken(credentials['token'],
3842+ credentials['token_secret'])
3843+ kwargs = dict(oauth_consumer=consumer, token=token,
3844+ http_method=http_method, http_url=url,
3845+ parameters=parameters)
3846+ get_request = oauth.OAuthRequest.from_consumer_and_token
3847+ oauth_req = get_request(**kwargs)
3848+ hmac_sha1 = oauth.OAuthSignatureMethod_HMAC_SHA1()
3849+ oauth_req.sign_request(hmac_sha1, consumer, token)
3850+ headers = oauth_req.to_header()
3851+
3852+ return headers
3853
3854=== added file 'ubuntu_sso/utils/tests/test_oauth_headers.py'
3855--- ubuntu_sso/utils/tests/test_oauth_headers.py 1970-01-01 00:00:00 +0000
3856+++ ubuntu_sso/utils/tests/test_oauth_headers.py 2011-08-25 19:16:19 +0000
3857@@ -0,0 +1,109 @@
3858+# -*- coding: utf-8 -*-
3859+
3860+# Author: Natalia B. Bidart <natalia.bidart@canonical.com>
3861+#
3862+# Copyright 2011 Canonical Ltd.
3863+#
3864+# This program is free software: you can redistribute it and/or modify it
3865+# under the terms of the GNU General Public License version 3, as published
3866+# by the Free Software Foundation.
3867+#
3868+# This program is distributed in the hope that it will be useful, but
3869+# WITHOUT ANY WARRANTY; without even the implied warranties of
3870+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
3871+# PURPOSE. See the GNU General Public License for more details.
3872+#
3873+# You should have received a copy of the GNU General Public License along
3874+# with this program. If not, see <http://www.gnu.org/licenses/>.
3875+
3876+"""Tests for the oauth_headers helper function."""
3877+
3878+from twisted.trial.unittest import TestCase
3879+
3880+from ubuntu_sso.utils import oauth, oauth_headers
3881+from ubuntu_sso.tests import TOKEN
3882+
3883+
3884+class FakedOAuthRequest(object):
3885+ """Replace the OAuthRequest class."""
3886+
3887+ params = {}
3888+
3889+ def __init__(self):
3890+ self.sign_request = lambda *args, **kwargs: None
3891+ self.to_header = lambda *args, **kwargs: {}
3892+
3893+ def from_consumer_and_token(oauth_consumer, **kwargs):
3894+ """Fake the method storing the params for check."""
3895+ FakedOAuthRequest.params.update(kwargs)
3896+ return FakedOAuthRequest()
3897+ from_consumer_and_token = staticmethod(from_consumer_and_token)
3898+
3899+
3900+class SignWithCredentialsTestCase(TestCase):
3901+ """Test suite for the oauth_headers method."""
3902+
3903+ url = u'http://example.com'
3904+
3905+ def build_header(self, url, http_method='GET'):
3906+ """Build an Oauth header for comparison."""
3907+ consumer = oauth.OAuthConsumer(TOKEN['consumer_key'],
3908+ TOKEN['consumer_secret'])
3909+ token = oauth.OAuthToken(TOKEN['token'],
3910+ TOKEN['token_secret'])
3911+ get_request = oauth.OAuthRequest.from_consumer_and_token
3912+ oauth_req = get_request(oauth_consumer=consumer, token=token,
3913+ http_method=http_method, http_url=url)
3914+ oauth_req.sign_request(oauth.OAuthSignatureMethod_HMAC_SHA1(),
3915+ consumer, token)
3916+ return oauth_req.to_header()
3917+
3918+ def dictify_header(self, header):
3919+ """Convert an OAuth header into a dict."""
3920+ result = {}
3921+ fields = header.split(', ')
3922+ for field in fields:
3923+ key, value = field.split('=')
3924+ result[key] = value.strip('"')
3925+
3926+ return result
3927+
3928+ def assert_header_equal(self, expected, actual):
3929+ """Is 'expected' equals to 'actual'?"""
3930+ expected = self.dictify_header(expected['Authorization'])
3931+ actual = self.dictify_header(actual['Authorization'])
3932+ for header in (expected, actual):
3933+ header.pop('oauth_nonce')
3934+ header.pop('oauth_timestamp')
3935+ header.pop('oauth_signature')
3936+
3937+ self.assertEqual(expected, actual)
3938+
3939+ def assert_method_called(self, path, query_str='', http_method='GET'):
3940+ """Assert that the url build by joining 'paths' was called."""
3941+ expected = (self.url, path, query_str)
3942+ expected = ''.join(expected).encode('utf8')
3943+ expected = self.build_header(expected, http_method=http_method)
3944+ actual = oauth_headers(url=self.url + path, credentials=TOKEN)
3945+ self.assert_header_equal(expected, actual)
3946+
3947+ def test_call(self):
3948+ """Calling 'get' triggers an OAuth signed GET request."""
3949+ path = u'/test/'
3950+ self.assert_method_called(path)
3951+
3952+ def test_quotes_path(self):
3953+ """Calling 'get' quotes the path."""
3954+ path = u'/test me more, sí!/'
3955+ self.assert_method_called(path)
3956+
3957+ def test_adds_parameters_to_oauth_request(self):
3958+ """The query string from the path is used in the oauth request."""
3959+ self.patch(oauth, 'OAuthRequest', FakedOAuthRequest)
3960+
3961+ path = u'/test/something?foo=bar'
3962+ oauth_headers(url=self.url + path, credentials=TOKEN)
3963+
3964+ self.assertIn('parameters', FakedOAuthRequest.params)
3965+ self.assertEqual(FakedOAuthRequest.params['parameters'],
3966+ {'foo': 'bar'})
3967
3968=== modified file 'ubuntu_sso/utils/txsecrets.py'
3969--- ubuntu_sso/utils/txsecrets.py 2011-07-22 20:12:20 +0000
3970+++ ubuntu_sso/utils/txsecrets.py 2011-08-25 19:16:19 +0000
3971@@ -21,13 +21,19 @@
3972 * http://code.confuego.org/secrets-xdg-specs/
3973 """
3974
3975-import gobject
3976+import sys
3977+# pylint: disable=E0611
3978+if 'gobject' in sys.modules:
3979+ import gobject as GObject
3980+else:
3981+ from gi.repository import GObject
3982+# pylint: enable=E0611
3983 import dbus
3984 from dbus.mainloop.glib import DBusGMainLoop
3985 import dbus.mainloop.glib
3986 from twisted.internet.defer import Deferred
3987
3988-gobject.threads_init()
3989+GObject.threads_init()
3990 dbus.mainloop.glib.threads_init()
3991 DBusGMainLoop(set_as_default=True)
3992
3993
3994=== modified file 'ubuntu_sso/utils/ui.py'
3995--- ubuntu_sso/utils/ui.py 2011-07-22 20:12:20 +0000
3996+++ ubuntu_sso/utils/ui.py 2011-08-25 19:16:19 +0000
3997@@ -20,10 +20,10 @@
3998
3999 import os
4000 import re
4001-import xdg
4002 import gettext
4003
4004 from ubuntu_sso.logger import setup_logging
4005+from ubuntu_sso import xdg_base_directory
4006
4007 logger = setup_logging('ubuntu_sso.utils.ui')
4008
4009@@ -35,6 +35,7 @@
4010 CAPTCHA_SOLUTION_ENTRY = _('Type the characters above')
4011 CAPTCHA_LOAD_ERROR = _('There was a problem getting the captcha, '
4012 'reloading...')
4013+CAPTCHA_REQUIRED_ERROR = _('The captcha is a required field')
4014 CONNECT_HELP_LABEL = _('To connect this computer to %(app_name)s ' \
4015 'enter your details below.')
4016 EMAIL1_ENTRY = _('Email address')
4017@@ -117,7 +118,7 @@
4018 return result
4019
4020 # no local data dir, looking within system data dirs
4021- data_dirs = xdg.BaseDirectory.xdg_data_dirs
4022+ data_dirs = xdg_base_directory.xdg_data_dirs
4023 for path in data_dirs:
4024 result = os.path.join(path, 'ubuntu-sso-client', 'data')
4025 result = os.path.abspath(result)
4026
4027=== added directory 'ubuntu_sso/xdg_base_directory'
4028=== added file 'ubuntu_sso/xdg_base_directory/__init__.py'
4029--- ubuntu_sso/xdg_base_directory/__init__.py 1970-01-01 00:00:00 +0000
4030+++ ubuntu_sso/xdg_base_directory/__init__.py 2011-08-25 19:16:19 +0000
4031@@ -0,0 +1,35 @@
4032+# Author: Natalia B. Bidart <natalia.bidart@canonical.com>
4033+#
4034+# Copyright 2011 Canonical Ltd.
4035+#
4036+# This program is free software: you can redistribute it and/or modify it
4037+# under the terms of the GNU General Public License version 3, as published
4038+# by the Free Software Foundation.
4039+#
4040+# This program is distributed in the hope that it will be useful, but
4041+# WITHOUT ANY WARRANTY; without even the implied warranties of
4042+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
4043+# PURPOSE. See the GNU General Public License for more details.
4044+#
4045+# You should have received a copy of the GNU General Public License along
4046+# with this program. If not, see <http://www.gnu.org/licenses/>.
4047+
4048+"""XDG multiplatform."""
4049+
4050+import sys
4051+
4052+# pylint: disable=C0103
4053+if sys.platform == "win32":
4054+ from ubuntu_sso.xdg_base_directory import windows
4055+ load_config_paths = windows.load_config_paths
4056+ save_config_path = windows.save_config_path
4057+ xdg_cache_home = windows.xdg_cache_home
4058+ xdg_data_home = windows.xdg_data_home
4059+ xdg_data_dirs = windows.xdg_data_dirs
4060+else:
4061+ import xdg.BaseDirectory
4062+ load_config_paths = xdg.BaseDirectory.load_config_paths
4063+ save_config_path = xdg.BaseDirectory.save_config_path
4064+ xdg_cache_home = xdg.BaseDirectory.xdg_cache_home
4065+ xdg_data_home = xdg.BaseDirectory.xdg_data_home
4066+ xdg_data_dirs = xdg.BaseDirectory.xdg_data_dirs
4067
4068=== added directory 'ubuntu_sso/xdg_base_directory/tests'
4069=== added file 'ubuntu_sso/xdg_base_directory/tests/__init__.py'
4070--- ubuntu_sso/xdg_base_directory/tests/__init__.py 1970-01-01 00:00:00 +0000
4071+++ ubuntu_sso/xdg_base_directory/tests/__init__.py 2011-08-25 19:16:19 +0000
4072@@ -0,0 +1,16 @@
4073+# ubuntu_sso - Ubuntu Single Sign On client support for desktop apps
4074+#
4075+# Copyright 2009-2010 Canonical Ltd.
4076+#
4077+# This program is free software: you can redistribute it and/or modify it
4078+# under the terms of the GNU General Public License version 3, as published
4079+# by the Free Software Foundation.
4080+#
4081+# This program is distributed in the hope that it will be useful, but
4082+# WITHOUT ANY WARRANTY; without even the implied warranties of
4083+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
4084+# PURPOSE. See the GNU General Public License for more details.
4085+#
4086+# You should have received a copy of the GNU General Public License along
4087+# with this program. If not, see <http://www.gnu.org/licenses/>.
4088+"""XDG tests."""
4089
4090=== added file 'ubuntu_sso/xdg_base_directory/tests/test_common.py'
4091--- ubuntu_sso/xdg_base_directory/tests/test_common.py 1970-01-01 00:00:00 +0000
4092+++ ubuntu_sso/xdg_base_directory/tests/test_common.py 2011-08-25 19:16:19 +0000
4093@@ -0,0 +1,49 @@
4094+# -*- coding: utf-8 -*-
4095+#
4096+# Authors: Natalia B. Bidart <natalia.bidart@canonical.com>
4097+#
4098+# Copyright 2011 Canonical Ltd.
4099+#
4100+# This program is free software: you can redistribute it and/or modify it
4101+# under the terms of the GNU General Public License version 3, as published
4102+# by the Free Software Foundation.
4103+#
4104+# This program is distributed in the hope that it will be useful, but
4105+# WITHOUT ANY WARRANTY; without even the implied warranties of
4106+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
4107+# PURPOSE. See the GNU General Public License for more details.
4108+#
4109+# You should have received a copy of the GNU General Public License along
4110+# with this program. If not, see <http://www.gnu.org/licenses/>.
4111+
4112+"""Platform independent tests for the XDG constants."""
4113+
4114+import os
4115+
4116+from twisted.trial.unittest import TestCase
4117+
4118+from ubuntu_sso import xdg_base_directory
4119+
4120+
4121+class TestBaseDirectory(TestCase):
4122+ """Tests for the BaseDirectory module."""
4123+
4124+ def test_xdg_cache_home_is_bytes(self):
4125+ """The returned path is bytes."""
4126+ actual = xdg_base_directory.xdg_cache_home
4127+ self.assertIsInstance(actual, str)
4128+
4129+ def test_xdg_data_home_is_bytes(self):
4130+ """The returned path is bytes."""
4131+ actual = xdg_base_directory.xdg_data_home
4132+ self.assertIsInstance(actual, str)
4133+
4134+ def test_load_config_paths_filter(self):
4135+ """Since those folders don't exist, this should be empty."""
4136+ self.assertEqual(list(xdg_base_directory.load_config_paths("x")), [])
4137+
4138+ def test_save_config_path(self):
4139+ """The path should end with xdg_config/x (respecting the separator)."""
4140+ self.patch(os, "makedirs", lambda *args: None)
4141+ result = xdg_base_directory.save_config_path("x")
4142+ self.assertEqual(result.split(os.sep)[-2:], ['xdg_config', 'x'])
4143
4144=== added file 'ubuntu_sso/xdg_base_directory/tests/test_windows.py'
4145--- ubuntu_sso/xdg_base_directory/tests/test_windows.py 1970-01-01 00:00:00 +0000
4146+++ ubuntu_sso/xdg_base_directory/tests/test_windows.py 2011-08-25 19:16:19 +0000
4147@@ -0,0 +1,112 @@
4148+# -*- coding: utf-8 -*-
4149+#
4150+# Authors: Natalia B. Bidart <natalia.bidart@canonical.com>
4151+#
4152+# Copyright 2011 Canonical Ltd.
4153+#
4154+# This program is free software: you can redistribute it and/or modify it
4155+# under the terms of the GNU General Public License version 3, as published
4156+# by the Free Software Foundation.
4157+#
4158+# This program is distributed in the hope that it will be useful, but
4159+# WITHOUT ANY WARRANTY; without even the implied warranties of
4160+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
4161+# PURPOSE. See the GNU General Public License for more details.
4162+#
4163+# You should have received a copy of the GNU General Public License along
4164+# with this program. If not, see <http://www.gnu.org/licenses/>.
4165+
4166+"""Windows-specific tests for the XDG constants."""
4167+
4168+import os
4169+import sys
4170+
4171+from twisted.trial.unittest import TestCase
4172+from ubuntuone.devtools.testcase import skipIfOS
4173+
4174+
4175+# pylint: disable=E1101, E0611, F0401
4176+if sys.platform == "win32":
4177+ import win32com.shell
4178+ from ubuntu_sso.xdg_base_directory.windows import (
4179+ get_config_dirs,
4180+ get_data_dirs,
4181+ get_special_folders,
4182+ )
4183+
4184+
4185+# pylint: disable=C0103
4186+class FakeShellConModule(object):
4187+ """Override CSIDL_ constants."""
4188+ CSIDL_PROFILE = 0
4189+ CSIDL_LOCAL_APPDATA = 1
4190+ CSIDL_COMMON_APPDATA = 2
4191+
4192+
4193+class FakeShellModule(object):
4194+
4195+ """Fake Shell Module."""
4196+
4197+ def __init__(self):
4198+ """Set the proper mapping between CSIDL_ consts."""
4199+ self.values = {
4200+ 0: u'c:\\path\\to\\users\\home',
4201+ 1: u'c:\\path\\to\\users\\home\\appData\\local',
4202+ 2: u'c:\\programData',
4203+ }
4204+
4205+ def SHGetFolderPath(self, dummy0, shellconValue, dummy2, dummy3):
4206+ """Override SHGetFolderPath functionality."""
4207+ return self.values[shellconValue]
4208+
4209+
4210+@skipIfOS('linux2', 'Windows-specific tests.')
4211+class TestBaseDirectoryWindows(TestCase):
4212+ """Tests for the BaseDirectory module."""
4213+
4214+ def test_get_special_folders(self):
4215+ """Make sure we can import the platform module."""
4216+
4217+ shellModule = FakeShellModule()
4218+ self.patch(win32com.shell, "shell", shellModule)
4219+ self.patch(win32com.shell, "shellcon", FakeShellConModule())
4220+ special_folders = get_special_folders()
4221+ self.assertTrue('Personal' in special_folders)
4222+ self.assertTrue('Local AppData' in special_folders)
4223+ self.assertTrue('AppData' in special_folders)
4224+ self.assertTrue('Common AppData' in special_folders)
4225+
4226+ self.assertTrue(special_folders['Personal'] == \
4227+ shellModule.values[FakeShellConModule.CSIDL_PROFILE])
4228+ self.assertTrue(special_folders['Local AppData'] == \
4229+ shellModule.values[FakeShellConModule.CSIDL_LOCAL_APPDATA])
4230+ self.assertTrue(special_folders['Local AppData'].startswith(
4231+ special_folders['AppData']))
4232+ self.assertTrue(special_folders['Common AppData'] == \
4233+ shellModule.values[FakeShellConModule.CSIDL_COMMON_APPDATA])
4234+
4235+ # Can't use assert_syncdaemon_path
4236+ for val in special_folders.itervalues():
4237+ self.assertIsInstance(val, str)
4238+ val.decode('utf8')
4239+ # Should not raise exceptions
4240+
4241+ def test_get_data_dirs(self):
4242+ """Check thet get_data_dirs uses pathsep correctly."""
4243+ bad_sep = filter(lambda x: x not in os.pathsep, ":;")
4244+ dir_list = ["A", "B", bad_sep, "C"]
4245+ self.patch(os, "environ",
4246+ dict(XDG_DATA_DIRS=os.pathsep.join(
4247+ dir_list)))
4248+ dirs = get_data_dirs()
4249+ self.assertEqual(dirs, dir_list)
4250+
4251+ def test_get_config_dirs(self):
4252+ """Check thet get_data_dirs uses pathsep correctly."""
4253+ bad_sep = filter(lambda x: x not in os.pathsep, ":;")
4254+ dir_list = ["A", "B", bad_sep, "C"]
4255+ self.patch(os, "environ",
4256+ dict(XDG_CONFIG_DIRS=os.pathsep.join(
4257+ dir_list)))
4258+ dirs = get_config_dirs()[1:]
4259+ self.assertEqual(dirs, dir_list)
4260
4261=== added file 'ubuntu_sso/xdg_base_directory/windows.py'
4262--- ubuntu_sso/xdg_base_directory/windows.py 1970-01-01 00:00:00 +0000
4263+++ ubuntu_sso/xdg_base_directory/windows.py 2011-08-25 19:16:19 +0000
4264@@ -0,0 +1,119 @@
4265+# Authors: Manuel de la Pena <manuel@canonical.com>
4266+# Diego Sarmentero <diego.sarmentero@canonical.com>
4267+#
4268+# Copyright 2011 Canonical Ltd.
4269+#
4270+# This program is free software: you can redistribute it and/or modify it
4271+# under the terms of the GNU General Public License version 3, as published
4272+# by the Free Software Foundation.
4273+#
4274+# This program is distributed in the hope that it will be useful, but
4275+# WITHOUT ANY WARRANTY; without even the implied warranties of
4276+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
4277+# PURPOSE. See the GNU General Public License for more details.
4278+#
4279+# You should have received a copy of the GNU General Public License along
4280+# with this program. If not, see <http://www.gnu.org/licenses/>.
4281+
4282+"""XDG helpers for windows."""
4283+
4284+import os
4285+
4286+
4287+# pylint: disable=C0103
4288+def get_special_folders():
4289+ """ Routine to grab all the Windows Special Folders locations.
4290+
4291+ If successful, returns dictionary
4292+ of shell folder locations indexed on Windows keyword for each;
4293+ otherwise, returns an empty dictionary.
4294+ """
4295+ # pylint: disable=W0621, F0401, E0611
4296+ special_folders = {}
4297+
4298+ from win32com.shell import shell, shellcon
4299+ # CSIDL_LOCAL_APPDATA = C:\Users\<username>\AppData\Local
4300+ # CSIDL_PROFILE = C:\Users\<username>
4301+ # CSIDL_COMMON_APPDATA = C:\ProgramData
4302+ # More information on these at
4303+ # http://msdn.microsoft.com/en-us/library/bb762494(v=vs.85).aspx
4304+ get_path = lambda name: shell.SHGetFolderPath(
4305+ 0, getattr(shellcon, name), None, 0).encode('utf8')
4306+ special_folders['Personal'] = get_path("CSIDL_PROFILE")
4307+ special_folders['Local AppData'] = get_path("CSIDL_LOCAL_APPDATA")
4308+ special_folders['AppData'] = os.path.dirname(
4309+ special_folders['Local AppData'])
4310+ special_folders['Common AppData'] = get_path("CSIDL_COMMON_APPDATA")
4311+ return special_folders
4312+
4313+special_folders = get_special_folders()
4314+
4315+home_path = special_folders['Personal']
4316+app_local_data_path = special_folders['Local AppData']
4317+app_global_data_path = special_folders['Common AppData']
4318+
4319+# use the non roaming app data
4320+xdg_data_home = os.environ.get('XDG_DATA_HOME',
4321+ os.path.join(app_local_data_path, 'xdg'))
4322+
4323+
4324+def get_data_dirs():
4325+ """Returns XDG data directories."""
4326+ return os.environ.get('XDG_DATA_DIRS',
4327+ '{0}{1}{2}'.format(app_local_data_path, os.pathsep,
4328+ app_global_data_path)).split(os.pathsep)
4329+
4330+xdg_data_dirs = get_data_dirs()
4331+
4332+# we will return the roaming data wich is as close as we get in windows
4333+# regarding caching.
4334+xdg_cache_home = os.environ.get('XDG_CACHE_HOME',
4335+ os.path.join(xdg_data_home, 'cache'))
4336+
4337+# point to the not roaming app data for the user
4338+xdg_config_home = os.environ.get('XDG_CONFIG_HOME',
4339+ app_local_data_path)
4340+
4341+
4342+def get_config_dirs():
4343+ """Return XDG config directories."""
4344+ return [xdg_config_home] + \
4345+ os.environ.get('XDG_CONFIG_DIRS',
4346+ app_global_data_path,
4347+ ).split(os.pathsep)
4348+
4349+xdg_config_dirs = get_config_dirs()
4350+
4351+xdg_data_dirs = filter(lambda x: x, xdg_data_dirs)
4352+xdg_config_dirs = filter(lambda x: x, xdg_config_dirs)
4353+
4354+
4355+def load_config_paths(*resource):
4356+ """Iterator of configuration paths.
4357+
4358+ Return an iterator which gives each directory named 'resource' in
4359+ the configuration search path. Information provided by earlier
4360+ directories should take precedence over later ones (ie, the user's
4361+ config dir comes first).
4362+ """
4363+ resource = os.path.join(*resource)
4364+ for config_dir in xdg_config_dirs:
4365+ path = os.path.join(config_dir, resource)
4366+ if os.path.exists(path):
4367+ yield path
4368+
4369+
4370+def save_config_path(*resource):
4371+ """Path to save configuration.
4372+
4373+ Ensure $XDG_CONFIG_HOME/<resource>/ exists, and return its path.
4374+ 'resource' should normally be the name of your application. Use this
4375+ when SAVING configuration settings. Use the xdg_config_dirs variable
4376+ for loading.
4377+ """
4378+ resource = os.path.join(*resource)
4379+ assert not resource.startswith('/')
4380+ path = os.path.join(xdg_config_home, resource)
4381+ if not os.path.isdir(path):
4382+ os.makedirs(path, 0700)
4383+ return path

Subscribers

People subscribed via source and target branches

to all changes: