Merge lp:~nataliabidart/ubuntu-sso-client/ubuntu-sso-client-0.98 into lp:ubuntu/maverick/ubuntu-sso-client

Proposed by Natalia Bidart on 2010-08-11
Status: Merged
Merged at revision: 3
Proposed branch: lp:~nataliabidart/ubuntu-sso-client/ubuntu-sso-client-0.98
Merge into: lp:ubuntu/maverick/ubuntu-sso-client
Diff against target: 5873 lines (+5329/-98)
28 files modified
MANIFEST.in (+7/-0)
PKG-INFO (+1/-1)
bin/show_nm_state.py (+39/-0)
bin/ubuntu-sso-login (+28/-23)
bin/ubuntu-sso-login-gui (+46/-0)
contrib/__init__.py (+18/-0)
contrib/dbus_util.py (+75/-0)
contrib/test (+146/-0)
contrib/testing/__init__.py (+1/-0)
contrib/testing/dbus-session.conf (+63/-0)
contrib/testing/testcase.py (+234/-0)
data/ui.glade (+689/-0)
debian/changelog (+7/-0)
debian/control (+10/-1)
debian/source/format (+1/-0)
run-tests (+9/-1)
setup.py (+9/-10)
ubuntu_sso/__init__.py (+6/-1)
ubuntu_sso/auth.py (+17/-12)
ubuntu_sso/gui.py (+797/-0)
ubuntu_sso/keyring.py (+111/-0)
ubuntu_sso/logger.py (+12/-2)
ubuntu_sso/main.py (+485/-33)
ubuntu_sso/networkstate.py (+106/-0)
ubuntu_sso/tests/test_gui.py (+1220/-0)
ubuntu_sso/tests/test_login.py (+188/-0)
ubuntu_sso/tests/test_main.py (+841/-14)
ubuntu_sso/tests/test_networkstate.py (+163/-0)
To merge this branch: bzr merge lp:~nataliabidart/ubuntu-sso-client/ubuntu-sso-client-0.98
Reviewer Review Type Date Requested Status
Ubuntu One hackers 2010-08-11 Pending
Review via email: mp+32362@code.launchpad.net

Description of the Change

Packaging for upstream version 0.98.

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=== added file 'MANIFEST.in'
2--- MANIFEST.in 1970-01-01 00:00:00 +0000
3+++ MANIFEST.in 2010-08-11 18:19:01 +0000
4@@ -0,0 +1,7 @@
5+include MANIFEST.in
6+include COPYING LICENSE.mocker README
7+include run-tests mocker.py
8+recursive-include bin *
9+recursive-include data *
10+recursive-include ubuntu_sso *.py
11+recursive-include contrib *.py *.conf test
12
13=== modified file 'PKG-INFO'
14--- PKG-INFO 2010-06-16 15:11:04 +0000
15+++ PKG-INFO 2010-08-11 18:19:01 +0000
16@@ -1,6 +1,6 @@
17 Metadata-Version: 1.0
18 Name: ubuntu-sso-client
19-Version: 0.0.3
20+Version: 0.98
21 Summary: Ubuntu Single Sign-On client
22 Home-page: https://launchpad.net/ubuntu-sso-client
23 Author: Natalia Bidart
24
25=== added file 'bin/show_nm_state.py'
26--- bin/show_nm_state.py 1970-01-01 00:00:00 +0000
27+++ bin/show_nm_state.py 2010-08-11 18:19:01 +0000
28@@ -0,0 +1,39 @@
29+# show_nm_state - Show the state of the NetworkManager daemon
30+#
31+# Author: Alejandro J. Cura <alecu@canonical.com>
32+#
33+# Copyright 2010 Canonical Ltd.
34+#
35+# This program is free software: you can redistribute it and/or modify it
36+# under the terms of the GNU General Public License version 3, as published
37+# by the Free Software Foundation.
38+#
39+# This program is distributed in the hope that it will be useful, but
40+# WITHOUT ANY WARRANTY; without even the implied warranties of
41+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
42+# PURPOSE. See the GNU General Public License for more details.
43+#
44+# You should have received a copy of the GNU General Public License along
45+# with this program. If not, see <http://www.gnu.org/licenses/>.
46+"""A test program that just shows the network state."""
47+
48+import gobject
49+
50+from dbus.mainloop.glib import DBusGMainLoop
51+
52+from ubuntu_sso.networkstate import NetworkManagerState, nm_state_names
53+
54+DBusGMainLoop(set_as_default=True)
55+loop = gobject.MainLoop()
56+
57+
58+def got_state(state):
59+ """Called when the network state is found."""
60+ try:
61+ print nm_state_names[state]
62+ finally:
63+ loop.quit()
64+
65+nms = NetworkManagerState(got_state)
66+nms.find_online_state()
67+loop.run()
68
69=== modified file 'bin/ubuntu-sso-login'
70--- bin/ubuntu-sso-login 2010-06-16 15:11:04 +0000
71+++ bin/ubuntu-sso-login 2010-08-11 18:19:01 +0000
72@@ -30,12 +30,13 @@
73
74 from dbus.mainloop.glib import DBusGMainLoop
75
76-from ubuntu_sso import DBUS_IFACE_AUTH_NAME
77-from ubuntu_sso.main import Login
78+from ubuntu_sso import DBUS_IFACE_AUTH_NAME, DBUS_BUS_NAME, DBUS_CRED_PATH
79+from ubuntu_sso.main import Login, SSOLogin, SSOCredentials
80
81 from ubuntu_sso.logger import setupLogging
82 logger = setupLogging("ubuntu-sso-login")
83
84+gtk.gdk.threads_init()
85 DBusGMainLoop(set_as_default=True)
86
87 _ = gettext.gettext
88@@ -55,6 +56,19 @@
89 }
90 widget_class '*Dialog*' style 'dialogs'
91 """
92+U1_PAIR_RECORD = "ubuntu_one_pair_record"
93+MAP_JS = """function(doc) {
94+ if (doc.service_name == "ubuntuone") {
95+ if (doc.application_annotations &&
96+ doc.application_annotations["Ubuntu One"] &&
97+ doc.application_annotations["Ubuntu One"]["%s"] &&
98+ doc.application_annotations["Ubuntu One"]["%s"]["deleted"]) {
99+ emit(doc._id, 1);
100+ } else {
101+ emit(doc._id, 0)
102+ }
103+ }
104+}""" % ("private_application_annotations", "private_application_annotations")
105
106
107 class LoginMain(object):
108@@ -83,7 +97,6 @@
109 signal_name="OAuthError",
110 dbus_interface=DBUS_IFACE_AUTH_NAME)
111
112-
113 def new_credentials(self, realm=None, consumer_key=None, sender=None):
114 """Signal callback for when we get new credentials."""
115 self.set_up_desktopcouch_pairing(consumer_key)
116@@ -173,23 +186,9 @@
117 return
118 # Check whether there is already a record of the Ubuntu One service
119 db = CouchDatabase("management", create=True)
120- if not db.view_exists("ubuntu_one_pair_record","ubuntu_one_pair_record"):
121- map_js = """function(doc) {
122- if (doc.service_name == "ubuntuone") {
123- if (doc.application_annotations &&
124- doc.application_annotations["Ubuntu One"] &&
125- doc.application_annotations["Ubuntu One"]["private_application_annotations"] &&
126- doc.application_annotations["Ubuntu One"]["private_application_annotations"]["deleted"]) {
127- emit(doc._id, 1);
128- } else {
129- emit(doc._id, 0)
130- }
131- }
132- }"""
133- db.add_view("ubuntu_one_pair_record", map_js, None,
134- "ubuntu_one_pair_record")
135- results = db.execute_view("ubuntu_one_pair_record",
136- "ubuntu_one_pair_record")
137+ if not db.view_exists(U1_PAIR_RECORD, U1_PAIR_RECORD):
138+ db.add_view(U1_PAIR_RECORD, MAP_JS, None, U1_PAIR_RECORD)
139+ results = db.execute_view(U1_PAIR_RECORD, U1_PAIR_RECORD)
140 found = False
141 # Results should contain either one row or no rows
142 # If there is one row, its value will be 0, meaning that there is
143@@ -224,7 +223,7 @@
144
145 # Register DBus service for making sure we run only one instance
146 bus = dbus.SessionBus()
147- name = bus.request_name(DBUS_IFACE_AUTH_NAME,
148+ name = bus.request_name(DBUS_BUS_NAME,
149 dbus.bus.NAME_FLAG_DO_NOT_QUEUE)
150 if name == dbus.bus.REQUEST_NAME_REPLY_EXISTS:
151 print _("Ubuntu One login manager already running, quitting")
152@@ -233,7 +232,13 @@
153 from twisted.internet import gtk2reactor
154 gtk2reactor.install()
155
156- login = Login(dbus.service.BusName(DBUS_IFACE_AUTH_NAME,
157- bus=dbus.SessionBus()))
158+ login = Login(dbus.service.BusName(DBUS_BUS_NAME,
159+ bus=dbus.SessionBus()))
160+ ssoLogin = SSOLogin(dbus.service.BusName(DBUS_BUS_NAME,
161+ bus=dbus.SessionBus()))
162+ SSOCredentials(dbus.service.BusName(DBUS_BUS_NAME,
163+ bus=dbus.SessionBus()),
164+ object_path=DBUS_CRED_PATH)
165+
166 manager = LoginMain()
167 manager.main()
168
169=== added file 'bin/ubuntu-sso-login-gui'
170--- bin/ubuntu-sso-login-gui 1970-01-01 00:00:00 +0000
171+++ bin/ubuntu-sso-login-gui 2010-08-11 18:19:01 +0000
172@@ -0,0 +1,46 @@
173+#!/usr/bin/python
174+
175+# ubuntu-sso-login-gui - GUI for registration and login for Ubuntu SSO
176+#
177+# Author: Natalia Bidart <natalia.bidart@canonical.com>
178+#
179+# Copyright 2010 Canonical Ltd.
180+#
181+# This program is free software: you can redistribute it and/or modify it
182+# under the terms of the GNU General Public License version 3, as published
183+# by the Free Software Foundation.
184+#
185+# This program is distributed in the hope that it will be useful, but
186+# WITHOUT ANY WARRANTY; without even the implied warranties of
187+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
188+# PURPOSE. See the GNU General Public License for more details.
189+#
190+# You should have received a copy of the GNU General Public License along
191+# with this program. If not, see <http://www.gnu.org/licenses/>.
192+"""Script to run the Ubuntu SSO client GUI."""
193+
194+import gtk
195+
196+from ubuntu_sso.gui import UbuntuSSOClientGUI
197+
198+
199+APP_NAME = 'Super Testing App'
200+TC_URI = 'http://ubuntu.com'
201+HELP_TEXT = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. ' \
202+ 'Nam sed lorem nibh. Suspendisse gravida nulla non nunc suscipit ' \
203+ 'pulvinar tempus ut augue.'
204+
205+main_quit = lambda *args, **kwargs: gtk.main_quit()
206+
207+
208+if __name__ == '__main__':
209+ win = gtk.Window()
210+ win.set_title(APP_NAME)
211+ win.set_position(gtk.WIN_POS_CENTER)
212+ win.set_size_request(800, 600)
213+ win.show()
214+ xid = win.window.xid
215+
216+ app = UbuntuSSOClientGUI(close_callback=main_quit, app_name=APP_NAME,
217+ tc_uri=TC_URI, help_text=HELP_TEXT, window_id=xid)
218+ gtk.main()
219
220=== added directory 'contrib'
221=== added file 'contrib/__init__.py'
222--- contrib/__init__.py 1970-01-01 00:00:00 +0000
223+++ contrib/__init__.py 2010-08-11 18:19:01 +0000
224@@ -0,0 +1,18 @@
225+# contrib - Extra required code to build/install the client
226+#
227+# Author: Rodney Dawes <rodney.dawes@canonical.com>
228+#
229+# Copyright 2009-2010 Canonical Ltd.
230+#
231+# This program is free software: you can redistribute it and/or modify it
232+# under the terms of the GNU General Public License version 3, as published
233+# by the Free Software Foundation.
234+#
235+# This program is distributed in the hope that it will be useful, but
236+# WITHOUT ANY WARRANTY; without even the implied warranties of
237+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
238+# PURPOSE. See the GNU General Public License for more details.
239+#
240+# You should have received a copy of the GNU General Public License along
241+# with this program. If not, see <http://www.gnu.org/licenses/>.
242+"""Extra things we need to build, test, or install the client."""
243
244=== added file 'contrib/dbus_util.py'
245--- contrib/dbus_util.py 1970-01-01 00:00:00 +0000
246+++ contrib/dbus_util.py 2010-08-11 18:19:01 +0000
247@@ -0,0 +1,75 @@
248+#
249+# Author: Guillermo Gonzalez <guillermo.gonzalez@canonical.com>
250+#
251+# Copyright 2009-2010 Canonical Ltd.
252+#
253+# This program is free software: you can redistribute it and/or modify it
254+# under the terms of the GNU General Public License version 3, as published
255+# by the Free Software Foundation.
256+#
257+# This program is distributed in the hope that it will be useful, but
258+# WITHOUT ANY WARRANTY; without even the implied warranties of
259+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
260+# PURPOSE. See the GNU General Public License for more details.
261+#
262+# You should have received a copy of the GNU General Public License along
263+# with this program. If not, see <http://www.gnu.org/licenses/>.
264+
265+import os
266+import signal
267+import subprocess
268+
269+from distutils.spawn import find_executable
270+
271+SRCDIR = os.environ.get('SRCDIR', os.getcwd())
272+
273+
274+class DBusLaunchError(Exception):
275+ """Error while launching dbus-daemon"""
276+ pass
277+
278+
279+class NotFoundError(Exception):
280+ """Not found error"""
281+ pass
282+
283+
284+class DBusRunner(object):
285+
286+ def __init__(self):
287+ self.dbus_address = None
288+ self.dbus_pid = None
289+ self.running = False
290+
291+ def startDBus(self):
292+ """Start our own session bus daemon for testing."""
293+ dbus = find_executable("dbus-daemon")
294+ if not dbus:
295+ raise NotFoundError("dbus-daemon was not found.")
296+
297+ config_file = os.path.join(os.path.abspath(SRCDIR),
298+ "contrib", "testing",
299+ "dbus-session.conf")
300+ dbus_args = ["--fork",
301+ "--config-file=" + config_file,
302+ "--print-address=1",
303+ "--print-pid=2"]
304+ p = subprocess.Popen([dbus] + dbus_args,
305+ bufsize=4096, stdout=subprocess.PIPE,
306+ stderr=subprocess.PIPE)
307+
308+ self.dbus_address = "".join(p.stdout.readlines()).strip()
309+ self.dbus_pid = int("".join(p.stderr.readlines()).strip())
310+
311+ if self.dbus_address != "":
312+ os.environ["DBUS_SESSION_BUS_ADDRESS"] = self.dbus_address
313+ else:
314+ os.kill(self.dbus_pid, signal.SIGKILL)
315+ raise DBusLaunchError("There was a problem launching dbus-daemon.")
316+ self.running = True
317+
318+ def stopDBus(self):
319+ """Stop our DBus session bus daemon."""
320+ del os.environ["DBUS_SESSION_BUS_ADDRESS"]
321+ os.kill(self.dbus_pid, signal.SIGKILL)
322+ self.running = False
323
324=== added file 'contrib/test'
325--- contrib/test 1970-01-01 00:00:00 +0000
326+++ contrib/test 2010-08-11 18:19:01 +0000
327@@ -0,0 +1,146 @@
328+#!/usr/bin/env python
329+#
330+# Author: Guillermo Gonzalez <guillermo.gonzalez@canonical.com>
331+#
332+# Copyright 2009-2010 Canonical Ltd.
333+#
334+# This program is free software: you can redistribute it and/or modify it
335+# under the terms of the GNU General Public License version 3, as published
336+# by the Free Software Foundation.
337+#
338+# This program is distributed in the hope that it will be useful, but
339+# WITHOUT ANY WARRANTY; without even the implied warranties of
340+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
341+# PURPOSE. See the GNU General Public License for more details.
342+#
343+# You should have received a copy of the GNU General Public License along
344+# with this program. If not, see <http://www.gnu.org/licenses/>.
345+
346+import os
347+import re
348+import signal
349+import sys
350+import string
351+import subprocess
352+import unittest
353+
354+sys.path.insert(0, os.path.abspath("."))
355+
356+from distutils.spawn import find_executable
357+
358+
359+class TestRunner(object):
360+
361+ def _load_unittest(self, relpath):
362+ """Load unit tests from a Python module with the given relative path."""
363+ assert relpath.endswith(".py"), (
364+ "%s does not appear to be a Python module" % relpath)
365+ modpath = relpath.replace(os.path.sep, ".")[:-3]
366+ module = __import__(modpath, None, None, [""])
367+
368+ # If the module has a 'suite' or 'test_suite' function, use that
369+ # to load the tests.
370+ if hasattr(module, "suite"):
371+ return module.suite()
372+ elif hasattr(module, "test_suite"):
373+ return module.test_suite()
374+ else:
375+ return unittest.defaultTestLoader.loadTestsFromModule(module)
376+
377+ def _collect_tests(self, testpath, test_pattern):
378+ """Return the set of unittests."""
379+ suite = unittest.TestSuite()
380+ if test_pattern:
381+ pattern = re.compile('.*%s.*' % test_pattern)
382+ else:
383+ pattern = None
384+
385+ if testpath:
386+ module_suite = self._load_unittest(testpath)
387+ if pattern:
388+ for inner_suite in module_suite._tests:
389+ for test in inner_suite._tests:
390+ if pattern.match(test.id()):
391+ suite.addTest(test)
392+ else:
393+ suite.addTests(module_suite)
394+ return suite
395+
396+ # We don't use the dirs variable, so ignore the warning
397+ # pylint: disable-msg=W0612
398+ for root, dirs, files in os.walk("ubuntu_sso"):
399+ for file in files:
400+ path = os.path.join(root, file)
401+ if file.endswith(".py") and file.startswith("test_"):
402+ module_suite = self._load_unittest(path)
403+ if pattern:
404+ for inner_suite in module_suite._tests:
405+ for test in inner_suite._tests:
406+ if pattern.match(test.id()):
407+ suite.addTest(test)
408+ else:
409+ suite.addTests(module_suite)
410+ return suite
411+
412+ def run(self, testpath, test_pattern=None, loops=None):
413+ """run the tests. """
414+ # install the glib2reactor before any import of the reactor to avoid
415+ # using the default SelectReactor and be able to run the dbus tests
416+ from twisted.internet import glib2reactor
417+ glib2reactor.install()
418+ from twisted.internet import reactor
419+ from twisted.trial.reporter import TreeReporter
420+ from twisted.trial.runner import TrialRunner
421+
422+ from contrib.dbus_util import DBusRunner
423+ dbus_runner = DBusRunner()
424+ dbus_runner.startDBus()
425+
426+ workingDirectory = os.path.join(os.getcwd(), "_trial_temp", "tmp")
427+ runner = TrialRunner(reporterFactory=TreeReporter, realTimeErrors=True,
428+ workingDirectory=workingDirectory)
429+
430+ # setup a custom XDG_CACHE_HOME and create the logs directory
431+ xdg_cache = os.path.join(os.getcwd(), "_trial_temp", "xdg_cache")
432+ os.environ["XDG_CACHE_HOME"] = xdg_cache
433+ # setup the ROOTDIR env var
434+ os.environ['ROOTDIR'] = os.getcwd()
435+ if not os.path.exists(xdg_cache):
436+ os.makedirs(xdg_cache)
437+ success = 0
438+ try:
439+ suite = self._collect_tests(testpath, test_pattern)
440+ if loops:
441+ old_suite = suite
442+ suite = unittest.TestSuite()
443+ for x in xrange(loops):
444+ suite.addTest(old_suite)
445+ result = runner.run(suite)
446+ success = result.wasSuccessful()
447+ finally:
448+ dbus_runner.stopDBus()
449+ if not success:
450+ sys.exit(1)
451+ else:
452+ sys.exit(0)
453+
454+
455+if __name__ == '__main__':
456+ from optparse import OptionParser
457+ usage = '%prog [options] path'
458+ parser = OptionParser(usage=usage)
459+ parser.add_option("-t", "--test", dest="test",
460+ help = "run specific tests, e.g: className.methodName")
461+ parser.add_option("-l", "--loop", dest="loops", type="int", default=1,
462+ help = "loop selected tests LOOPS number of times",
463+ metavar="LOOPS")
464+
465+ (options, args) = parser.parse_args()
466+ if args:
467+ testpath = args[0]
468+ if not os.path.exists(testpath):
469+ print "the path to test does not exists!"
470+ sys.exit()
471+ else:
472+ testpath = None
473+ TestRunner().run(testpath, options.test, options.loops)
474
475=== added directory 'contrib/testing'
476=== added file 'contrib/testing/__init__.py'
477--- contrib/testing/__init__.py 1970-01-01 00:00:00 +0000
478+++ contrib/testing/__init__.py 2010-08-11 18:19:01 +0000
479@@ -0,0 +1,1 @@
480+"""Testing utilities for Ubuntu One client code."""
481
482=== added file 'contrib/testing/dbus-session.conf'
483--- contrib/testing/dbus-session.conf 1970-01-01 00:00:00 +0000
484+++ contrib/testing/dbus-session.conf 2010-08-11 18:19:01 +0000
485@@ -0,0 +1,63 @@
486+<!-- This configuration file controls our test-only session bus -->
487+
488+<!DOCTYPE busconfig PUBLIC "-//freedesktop//DTD D-Bus Bus Configuration 1.0//EN"
489+ "http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
490+<busconfig>
491+ <!-- We only use a session bus -->
492+ <type>session</type>
493+
494+ <listen>unix:tmpdir=/tmp</listen>
495+
496+ <!-- Load our own services.
497+ To make other dbus service in this session bus, just add another servicedir entry. -->
498+ <servicedir>dbus-session</servicedir>
499+ <!-- Load the standard session services -->
500+ <!--standard_session_servicedirs /-->
501+
502+ <policy context="default">
503+ <!-- Allow everything to be sent -->
504+ <allow send_destination="*" eavesdrop="true"/>
505+ <!-- Allow everything to be received -->
506+ <allow eavesdrop="true"/>
507+ <!-- Allow anyone to own anything -->
508+ <allow own="*"/>
509+ </policy>
510+
511+ <!-- Config files are placed here that among other things,
512+ further restrict the above policy for specific services. -->
513+ <includedir>/etc/dbus-1/session.d</includedir>
514+
515+ <!-- raise the service start timeout to 40 seconds as it can timeout
516+ on the live cd on slow machines -->
517+ <limit name="service_start_timeout">60000</limit>
518+
519+ <!-- This is included last so local configuration can override what's
520+ in this standard file -->
521+ <include ignore_missing="yes">session-local.conf</include>
522+
523+ <include ignore_missing="yes" if_selinux_enabled="yes" selinux_root_relative="yes">contexts/dbus_contexts</include>
524+
525+ <!-- For the session bus, override the default relatively-low limits
526+ with essentially infinite limits, since the bus is just running
527+ as the user anyway, using up bus resources is not something we need
528+ to worry about. In some cases, we do set the limits lower than
529+ "all available memory" if exceeding the limit is almost certainly a bug,
530+ having the bus enforce a limit is nicer than a huge memory leak. But the
531+ intent is that these limits should never be hit. -->
532+
533+ <!-- the memory limits are 1G instead of say 4G because they can't exceed 32-bit signed int max -->
534+ <limit name="max_incoming_bytes">1000000000</limit>
535+ <limit name="max_outgoing_bytes">1000000000</limit>
536+ <limit name="max_message_size">1000000000</limit>
537+ <limit name="service_start_timeout">120000</limit>
538+ <limit name="auth_timeout">240000</limit>
539+ <limit name="max_completed_connections">100000</limit>
540+ <limit name="max_incomplete_connections">10000</limit>
541+ <limit name="max_connections_per_user">100000</limit>
542+ <limit name="max_pending_service_starts">10000</limit>
543+ <limit name="max_names_per_connection">50000</limit>
544+ <limit name="max_match_rules_per_connection">50000</limit>
545+ <limit name="max_replies_per_connection">50000</limit>
546+ <limit name="reply_timeout">300000</limit>
547+
548+</busconfig>
549
550=== added file 'contrib/testing/testcase.py'
551--- contrib/testing/testcase.py 1970-01-01 00:00:00 +0000
552+++ contrib/testing/testcase.py 2010-08-11 18:19:01 +0000
553@@ -0,0 +1,234 @@
554+#
555+# Author: Guillermo Gonzalez <guillermo.gonzalez@canonical.com>
556+#
557+# Copyright 2009-2010 Canonical Ltd.
558+#
559+# This program is free software: you can redistribute it and/or modify it
560+# under the terms of the GNU General Public License version 3, as published
561+# by the Free Software Foundation.
562+#
563+# This program is distributed in the hope that it will be useful, but
564+# WITHOUT ANY WARRANTY; without even the implied warranties of
565+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
566+# PURPOSE. See the GNU General Public License for more details.
567+#
568+# You should have received a copy of the GNU General Public License along
569+# with this program. If not, see <http://www.gnu.org/licenses/>.
570+""" Base tests cases and test utilities """
571+from __future__ import with_statement
572+
573+import dbus
574+from dbus.mainloop.glib import DBusGMainLoop
575+import contextlib
576+import logging
577+import os
578+import shutil
579+import itertools
580+
581+from ubuntu_sso import DBUS_IFACE_AUTH_NAME
582+from ubuntu_sso.main import Login, LoginProcessor
583+
584+from twisted.internet import defer
585+from twisted.python import failure
586+from unittest import TestCase
587+from zope.interface import implements
588+from oauth import oauth
589+
590+
591+@contextlib.contextmanager
592+def environ(env_var, new_value):
593+ """context manager to replace/add an environ value"""
594+ old_value = os.environ.get(env_var, None)
595+ os.environ[env_var] = new_value
596+ yield
597+ if old_value is None:
598+ os.environ.pop(env_var)
599+ else:
600+ os.environ[env_var] = old_value
601+
602+
603+class BaseTestCase(TestCase):
604+ """Base TestCase with helper methods to handle temp dir.
605+
606+ This class provides:
607+ mktemp(name): helper to create temporary dirs
608+ rmtree(path): support read-only shares
609+ makedirs(path): support read-only shares
610+ """
611+
612+ def mktemp(self, name='temp'):
613+ """Customized mktemp that accepts an optional name argument. """
614+ tempdir = os.path.join(self.tmpdir, name)
615+ if os.path.exists(tempdir):
616+ self.rmtree(tempdir)
617+ self.makedirs(tempdir)
618+ return tempdir
619+
620+ @property
621+ def tmpdir(self):
622+ """Default tmpdir: module/class/test_method."""
623+ # check if we already generated the root path
624+ root_dir = getattr(self, '__root', None)
625+ if root_dir:
626+ return root_dir
627+ MAX_FILENAME = 32 # some platforms limit lengths of filenames
628+ base = os.path.join(self.__class__.__module__[:MAX_FILENAME],
629+ self.__class__.__name__[:MAX_FILENAME],
630+ self._testMethodName[:MAX_FILENAME])
631+ # use _trial_temp dir, it should be os.gwtcwd()
632+ # define the root temp dir of the testcase, pylint: disable-msg=W0201
633+ self.__root = os.path.join(os.getcwd(), base)
634+ return self.__root
635+
636+ def rmtree(self, path):
637+ """Custom rmtree that handle ro parent(s) and childs."""
638+ if not os.path.exists(path):
639+ return
640+ # change perms to rw, so we can delete the temp dir
641+ if path != getattr(self, '__root', None):
642+ os.chmod(os.path.dirname(path), 0755)
643+ if not os.access(path, os.W_OK):
644+ os.chmod(path, 0755)
645+ # pylint: disable-msg=W0612
646+ for dirpath, dirs, files in os.walk(path):
647+ for dir in dirs:
648+ if not os.access(os.path.join(dirpath, dir), os.W_OK):
649+ os.chmod(os.path.join(dirpath, dir), 0777)
650+ shutil.rmtree(path)
651+
652+ def makedirs(self, path):
653+ """Custom makedirs that handle ro parent."""
654+ parent = os.path.dirname(path)
655+ if os.path.exists(parent):
656+ os.chmod(parent, 0755)
657+ os.makedirs(path)
658+
659+
660+class DBusTestCase(BaseTestCase):
661+ """ Test the DBus event handling """
662+
663+ def setUp(self):
664+ """ Setup the infrastructure fo the test (dbus service). """
665+ BaseTestCase.setUp(self)
666+ self.loop = DBusGMainLoop(set_as_default=True)
667+ self.bus = dbus.bus.BusConnection(mainloop=self.loop)
668+ # monkeypatch busName.__del__ to avoid errors on gc
669+ # we take care of releasing the name in shutdown
670+ dbus.service.BusName.__del__ = lambda _: None
671+ self.bus.set_exit_on_disconnect(False)
672+ self.signal_receivers = set()
673+
674+ def tearDown(self):
675+ """ Cleanup the test. """
676+ d = self.cleanup_signal_receivers(self.signal_receivers)
677+ d.addBoth(self._tearDown)
678+ d.addBoth(lambda _: BaseTestCase.tearDown(self))
679+ return d
680+
681+ def _tearDown(self, *args):
682+ """ shutdown """
683+ self.dbus_iface.bus.flush()
684+ self.bus.flush()
685+ self.bus.close()
686+
687+ def error_handler(self, error):
688+ """ default error handler for DBus calls. """
689+ if isinstance(error, failure.Failure):
690+ self.fail(error.getErrorMessage())
691+
692+ def cleanup_signal_receivers(self, signal_receivers):
693+ """ cleanup self.signal_receivers and returns a deferred """
694+ deferreds = []
695+ for match in signal_receivers:
696+ d = defer.Deferred()
697+
698+ def callback(*args):
699+ """ callback that accepts *args. """
700+ if not d.called:
701+ d.callback(args)
702+
703+ self.bus.call_async(dbus.bus.BUS_DAEMON_NAME,
704+ dbus.bus.BUS_DAEMON_PATH,
705+ dbus.bus.BUS_DAEMON_IFACE, 'RemoveMatch', 's',
706+ (str(match),), callback, self.error_handler)
707+ deferreds.append(d)
708+ if deferreds:
709+ return defer.DeferredList(deferreds)
710+ else:
711+ return defer.succeed(True)
712+
713+
714+class FakeLoginProcessor(LoginProcessor):
715+ """Stub login processor."""
716+
717+ def __init__(self, dbus_object):
718+ """Initialize the login processor."""
719+ LoginProcessor.__init__(self, dbus_object)
720+ self.next_login_cb = None
721+
722+ def login(self, realm, consumer_key, error_handler=None,
723+ reply_handler=None, do_login=True):
724+ """Stub, call self.next_login_cb or send NewCredentials if
725+ self.next_login_cb isn't defined.
726+ """
727+ self.realm = str(realm)
728+ self.consumer_key = str(consumer_key)
729+ if self.next_login_cb:
730+ cb = self.next_login_cb[0]
731+ args = self.next_login_cb[1]
732+ self.next_login_cb = None
733+ return cb(*args)
734+ else:
735+ self.dbus_object.NewCredentials(realm, consumer_key)
736+
737+ def clear_token(self, realm, consumer_key):
738+ """Stub, do nothing"""
739+ pass
740+
741+ def next_login_with(self, callback, args=tuple()):
742+ """shortcircuit the next call to login and call the specified callback.
743+ callback is usually one of: self.got_token, self.got_no_token,
744+ self.got_denial or self.got_error.
745+ """
746+ self.next_login_cb = (callback, args)
747+
748+
749+class FakeLogin(Login):
750+ """Stub Object which listens for D-Bus OAuth requests"""
751+
752+ def __init__(self, bus):
753+ """Initiate the object."""
754+ self.bus = bus
755+ self.busName = dbus.service.BusName(DBUS_IFACE_AUTH_NAME, bus=self.bus)
756+ # bypass the parent class __init__ as it has the path hardcoded
757+ # and we can't use '/' as the path, as we are already using it
758+ # for syncdaemon. pylint: disable-msg=W0233,W0231
759+ dbus.service.Object.__init__(self, object_path="/oauthdesktop",
760+ bus_name=self.busName)
761+ self.processor = FakeLoginProcessor(self)
762+ self.currently_authing = False
763+
764+ def shutdown(self):
765+ """Shutdown and remove any trace from the bus"""
766+ self.busName.get_bus().release_name(self.busName.get_name())
767+ self.remove_from_connection()
768+
769+
770+class MementoHandler(logging.Handler):
771+ """A handler class which store logging records in a list."""
772+
773+ def __init__(self, *args, **kwargs):
774+ """Create the instance, and add a records attribute."""
775+ logging.Handler.__init__(self, *args, **kwargs)
776+ self.records = []
777+
778+ def emit(self, record):
779+ """Just add the record to self.records."""
780+ self.records.append(record)
781+
782+ def check(self, level, msg):
783+ """Check that something is logged."""
784+ for rec in self.records:
785+ if rec.levelno == level and str(msg) in rec.message:
786+ return True
787+ return False
788
789=== added file 'data/email.png'
790Binary files data/email.png 1970-01-01 00:00:00 +0000 and data/email.png 2010-08-11 18:19:01 +0000 differ
791=== added file 'data/ui.glade'
792--- data/ui.glade 1970-01-01 00:00:00 +0000
793+++ data/ui.glade 2010-08-11 18:19:01 +0000
794@@ -0,0 +1,689 @@
795+<?xml version="1.0"?>
796+<interface>
797+ <requires lib="gtk+" version="2.16"/>
798+ <!-- interface-naming-policy project-wide -->
799+ <object class="GtkWindow" id="window">
800+ <property name="border_width">20</property>
801+ <property name="window_position">center</property>
802+ <signal name="delete_event" handler="on_close_clicked"/>
803+ <child>
804+ <object class="GtkVBox" id="window_vbox">
805+ <property name="visible">True</property>
806+ <property name="spacing">10</property>
807+ <child>
808+ <object class="GtkLabel" id="header_label">
809+ <property name="visible">True</property>
810+ <property name="xalign">0</property>
811+ <property name="label" translatable="yes">Header Label </property>
812+ <property name="wrap">True</property>
813+ </object>
814+ <packing>
815+ <property name="expand">False</property>
816+ <property name="padding">10</property>
817+ <property name="position">0</property>
818+ </packing>
819+ </child>
820+ <child>
821+ <object class="GtkLabel" id="help_label">
822+ <property name="visible">True</property>
823+ <property name="xalign">0</property>
824+ <property name="label" translatable="yes">help label</property>
825+ <property name="wrap">True</property>
826+ </object>
827+ <packing>
828+ <property name="expand">False</property>
829+ <property name="position">1</property>
830+ </packing>
831+ </child>
832+ <child>
833+ <object class="GtkLabel" id="warning_label">
834+ <property name="visible">True</property>
835+ <property name="xalign">0</property>
836+ <property name="label" translatable="yes">warning label</property>
837+ <property name="wrap">True</property>
838+ </object>
839+ <packing>
840+ <property name="expand">False</property>
841+ <property name="position">2</property>
842+ </packing>
843+ </child>
844+ <child>
845+ <placeholder/>
846+ </child>
847+ </object>
848+ </child>
849+ </object>
850+ <object class="GtkVBox" id="enter_details_vbox">
851+ <property name="visible">True</property>
852+ <property name="spacing">10</property>
853+ <child>
854+ <placeholder/>
855+ </child>
856+ <child>
857+ <object class="GtkLabel" id="name_warning_label">
858+ <property name="visible">True</property>
859+ <property name="wrap">True</property>
860+ </object>
861+ <packing>
862+ <property name="expand">False</property>
863+ <property name="position">1</property>
864+ </packing>
865+ </child>
866+ <child>
867+ <object class="GtkHBox" id="emails_hbox">
868+ <property name="visible">True</property>
869+ <property name="spacing">5</property>
870+ <property name="homogeneous">True</property>
871+ <child>
872+ <placeholder/>
873+ </child>
874+ <child>
875+ <placeholder/>
876+ </child>
877+ </object>
878+ <packing>
879+ <property name="expand">False</property>
880+ <property name="position">2</property>
881+ </packing>
882+ </child>
883+ <child>
884+ <object class="GtkLabel" id="email_warning_label">
885+ <property name="visible">True</property>
886+ <property name="wrap">True</property>
887+ </object>
888+ <packing>
889+ <property name="expand">False</property>
890+ <property name="position">3</property>
891+ </packing>
892+ </child>
893+ <child>
894+ <object class="GtkHBox" id="passwords_hbox">
895+ <property name="visible">True</property>
896+ <property name="spacing">5</property>
897+ <property name="homogeneous">True</property>
898+ <child>
899+ <placeholder/>
900+ </child>
901+ <child>
902+ <placeholder/>
903+ </child>
904+ </object>
905+ <packing>
906+ <property name="expand">False</property>
907+ <property name="position">4</property>
908+ </packing>
909+ </child>
910+ <child>
911+ <object class="GtkLabel" id="password_help_label">
912+ <property name="visible">True</property>
913+ <property name="wrap">True</property>
914+ </object>
915+ <packing>
916+ <property name="expand">False</property>
917+ <property name="position">5</property>
918+ </packing>
919+ </child>
920+ <child>
921+ <object class="GtkLabel" id="password_warning_label">
922+ <property name="visible">True</property>
923+ <property name="wrap">True</property>
924+ </object>
925+ <packing>
926+ <property name="position">6</property>
927+ </packing>
928+ </child>
929+ <child>
930+ <placeholder/>
931+ </child>
932+ <child>
933+ <object class="GtkEventBox" id="captcha_loading">
934+ <property name="width_request">300</property>
935+ <property name="height_request">60</property>
936+ <property name="visible">True</property>
937+ <child>
938+ <placeholder/>
939+ </child>
940+ </object>
941+ <packing>
942+ <property name="expand">False</property>
943+ <property name="fill">False</property>
944+ <property name="position">8</property>
945+ </packing>
946+ </child>
947+ <child>
948+ <object class="GtkImage" id="captcha_image">
949+ <property name="visible">True</property>
950+ <property name="stock">gtk-missing-image</property>
951+ </object>
952+ <packing>
953+ <property name="position">9</property>
954+ </packing>
955+ </child>
956+ <child>
957+ <object class="GtkVBox" id="captcha_solution_vbox">
958+ <property name="visible">True</property>
959+ <property name="spacing">10</property>
960+ <child>
961+ <placeholder/>
962+ </child>
963+ <child>
964+ <object class="GtkLabel" id="captcha_solution_warning_label">
965+ <property name="visible">True</property>
966+ <property name="wrap">True</property>
967+ </object>
968+ <packing>
969+ <property name="position">1</property>
970+ </packing>
971+ </child>
972+ </object>
973+ <packing>
974+ <property name="position">10</property>
975+ </packing>
976+ </child>
977+ <child>
978+ <object class="GtkCheckButton" id="yes_to_updates_checkbutton">
979+ <property name="visible">True</property>
980+ <property name="can_focus">True</property>
981+ <property name="receives_default">False</property>
982+ <property name="active">True</property>
983+ <property name="draw_indicator">True</property>
984+ </object>
985+ <packing>
986+ <property name="expand">False</property>
987+ <property name="position">11</property>
988+ </packing>
989+ </child>
990+ <child>
991+ <object class="GtkHBox" id="hbox1">
992+ <property name="visible">True</property>
993+ <child>
994+ <object class="GtkCheckButton" id="yes_to_tc_checkbutton">
995+ <property name="label" translatable="yes">Yes! I agree with</property>
996+ <property name="visible">True</property>
997+ <property name="can_focus">True</property>
998+ <property name="receives_default">False</property>
999+ <property name="active">True</property>
1000+ <property name="draw_indicator">True</property>
1001+ </object>
1002+ <packing>
1003+ <property name="expand">False</property>
1004+ <property name="position">0</property>
1005+ </packing>
1006+ </child>
1007+ <child>
1008+ <object class="GtkLinkButton" id="tc_button">
1009+ <property name="label" translatable="yes">button</property>
1010+ <property name="visible">True</property>
1011+ <property name="can_focus">True</property>
1012+ <property name="receives_default">True</property>
1013+ <property name="has_tooltip">True</property>
1014+ <property name="relief">none</property>
1015+ <signal name="clicked" handler="on_tc_button_clicked"/>
1016+ </object>
1017+ <packing>
1018+ <property name="expand">False</property>
1019+ <property name="position">1</property>
1020+ </packing>
1021+ </child>
1022+ </object>
1023+ <packing>
1024+ <property name="expand">False</property>
1025+ <property name="position">12</property>
1026+ </packing>
1027+ </child>
1028+ <child>
1029+ <object class="GtkLabel" id="tc_warning_label">
1030+ <property name="visible">True</property>
1031+ <property name="wrap">True</property>
1032+ </object>
1033+ <packing>
1034+ <property name="position">13</property>
1035+ </packing>
1036+ </child>
1037+ <child>
1038+ <object class="GtkHButtonBox" id="hbuttonbox1">
1039+ <property name="visible">True</property>
1040+ <property name="spacing">5</property>
1041+ <property name="layout_style">end</property>
1042+ <child>
1043+ <object class="GtkButton" id="join_cancel_button">
1044+ <property name="label">gtk-cancel</property>
1045+ <property name="visible">True</property>
1046+ <property name="can_focus">True</property>
1047+ <property name="receives_default">True</property>
1048+ <property name="use_stock">True</property>
1049+ </object>
1050+ <packing>
1051+ <property name="expand">False</property>
1052+ <property name="fill">False</property>
1053+ <property name="position">0</property>
1054+ </packing>
1055+ </child>
1056+ <child>
1057+ <object class="GtkButton" id="join_ok_button">
1058+ <property name="label">gtk-go-forward</property>
1059+ <property name="visible">True</property>
1060+ <property name="can_focus">True</property>
1061+ <property name="receives_default">True</property>
1062+ <property name="use_stock">True</property>
1063+ <signal name="clicked" handler="on_join_ok_button_clicked"/>
1064+ </object>
1065+ <packing>
1066+ <property name="expand">False</property>
1067+ <property name="fill">False</property>
1068+ <property name="position">1</property>
1069+ </packing>
1070+ </child>
1071+ </object>
1072+ <packing>
1073+ <property name="expand">False</property>
1074+ <property name="position">14</property>
1075+ </packing>
1076+ </child>
1077+ <child>
1078+ <object class="GtkLinkButton" id="login_button">
1079+ <property name="label" translatable="yes">button</property>
1080+ <property name="visible">True</property>
1081+ <property name="can_focus">True</property>
1082+ <property name="receives_default">True</property>
1083+ <property name="has_tooltip">True</property>
1084+ <property name="relief">none</property>
1085+ <signal name="clicked" handler="on_sign_in_button_clicked"/>
1086+ </object>
1087+ <packing>
1088+ <property name="expand">False</property>
1089+ <property name="position">15</property>
1090+ </packing>
1091+ </child>
1092+ </object>
1093+ <object class="GtkVBox" id="processing_vbox">
1094+ <property name="visible">True</property>
1095+ <property name="spacing">10</property>
1096+ <child>
1097+ <placeholder/>
1098+ </child>
1099+ </object>
1100+ <object class="GtkVBox" id="verify_email_vbox">
1101+ <property name="visible">True</property>
1102+ <property name="spacing">10</property>
1103+ <child>
1104+ <object class="GtkImage" id="verify_email_image">
1105+ <property name="visible">True</property>
1106+ <property name="stock">gtk-missing-image</property>
1107+ </object>
1108+ <packing>
1109+ <property name="position">0</property>
1110+ </packing>
1111+ </child>
1112+ <child>
1113+ <placeholder/>
1114+ </child>
1115+ <child>
1116+ <object class="GtkHButtonBox" id="hbuttonbox2">
1117+ <property name="visible">True</property>
1118+ <property name="spacing">5</property>
1119+ <property name="layout_style">end</property>
1120+ <child>
1121+ <object class="GtkButton" id="verify_token_button">
1122+ <property name="label">gtk-ok</property>
1123+ <property name="visible">True</property>
1124+ <property name="can_focus">True</property>
1125+ <property name="receives_default">True</property>
1126+ <property name="use_stock">True</property>
1127+ <signal name="clicked" handler="on_verify_token_button_clicked"/>
1128+ </object>
1129+ <packing>
1130+ <property name="expand">False</property>
1131+ <property name="fill">False</property>
1132+ <property name="position">0</property>
1133+ </packing>
1134+ </child>
1135+ </object>
1136+ <packing>
1137+ <property name="expand">False</property>
1138+ <property name="position">2</property>
1139+ </packing>
1140+ </child>
1141+ </object>
1142+ <object class="GtkVBox" id="tc_browser_vbox">
1143+ <property name="visible">True</property>
1144+ <child>
1145+ <object class="GtkScrolledWindow" id="tc_browser_window">
1146+ <property name="visible">True</property>
1147+ <property name="can_focus">True</property>
1148+ <property name="border_width">10</property>
1149+ <property name="hscrollbar_policy">never</property>
1150+ <property name="vscrollbar_policy">automatic</property>
1151+ <child>
1152+ <placeholder/>
1153+ </child>
1154+ </object>
1155+ <packing>
1156+ <property name="position">0</property>
1157+ </packing>
1158+ </child>
1159+ <child>
1160+ <object class="GtkHButtonBox" id="hbuttonbox4">
1161+ <property name="visible">True</property>
1162+ <property name="layout_style">end</property>
1163+ <child>
1164+ <object class="GtkButton" id="tc_back_button">
1165+ <property name="label">gtk-go-back</property>
1166+ <property name="visible">True</property>
1167+ <property name="can_focus">True</property>
1168+ <property name="receives_default">True</property>
1169+ <property name="use_stock">True</property>
1170+ <signal name="clicked" handler="on_tc_back_button_clicked"/>
1171+ </object>
1172+ <packing>
1173+ <property name="expand">False</property>
1174+ <property name="fill">False</property>
1175+ <property name="position">0</property>
1176+ </packing>
1177+ </child>
1178+ </object>
1179+ <packing>
1180+ <property name="expand">False</property>
1181+ <property name="position">1</property>
1182+ </packing>
1183+ </child>
1184+ </object>
1185+ <object class="GtkVBox" id="login_vbox">
1186+ <property name="visible">True</property>
1187+ <property name="spacing">10</property>
1188+ <child>
1189+ <object class="GtkAlignment" id="alignment3">
1190+ <property name="visible">True</property>
1191+ <property name="xscale">0</property>
1192+ <property name="yscale">0</property>
1193+ <child>
1194+ <object class="GtkVBox" id="login_details_vbox">
1195+ <property name="visible">True</property>
1196+ <property name="spacing">5</property>
1197+ <child>
1198+ <placeholder/>
1199+ </child>
1200+ <child>
1201+ <placeholder/>
1202+ </child>
1203+ <child>
1204+ <object class="GtkLabel" id="login_email_warning_label">
1205+ <property name="visible">True</property>
1206+ <property name="label" translatable="yes">label</property>
1207+ </object>
1208+ <packing>
1209+ <property name="position">2</property>
1210+ </packing>
1211+ </child>
1212+ <child>
1213+ <object class="GtkLabel" id="login_password_warning_label">
1214+ <property name="visible">True</property>
1215+ <property name="label" translatable="yes">label</property>
1216+ </object>
1217+ <packing>
1218+ <property name="position">4</property>
1219+ </packing>
1220+ </child>
1221+ <child>
1222+ <object class="GtkLinkButton" id="forgotten_password_button">
1223+ <property name="label" translatable="yes">button</property>
1224+ <property name="visible">True</property>
1225+ <property name="can_focus">True</property>
1226+ <property name="receives_default">True</property>
1227+ <property name="has_tooltip">True</property>
1228+ <property name="relief">none</property>
1229+ <signal name="clicked" handler="on_forgotten_password_button_clicked"/>
1230+ </object>
1231+ <packing>
1232+ <property name="position">5</property>
1233+ </packing>
1234+ </child>
1235+ </object>
1236+ </child>
1237+ </object>
1238+ <packing>
1239+ <property name="position">0</property>
1240+ </packing>
1241+ </child>
1242+ <child>
1243+ <object class="GtkHButtonBox" id="hbuttonbox5">
1244+ <property name="visible">True</property>
1245+ <property name="spacing">5</property>
1246+ <property name="layout_style">end</property>
1247+ <child>
1248+ <object class="GtkButton" id="login_cancel_button">
1249+ <property name="label">gtk-cancel</property>
1250+ <property name="visible">True</property>
1251+ <property name="can_focus">True</property>
1252+ <property name="receives_default">True</property>
1253+ <property name="use_stock">True</property>
1254+ </object>
1255+ <packing>
1256+ <property name="expand">False</property>
1257+ <property name="fill">False</property>
1258+ <property name="position">0</property>
1259+ </packing>
1260+ </child>
1261+ <child>
1262+ <object class="GtkButton" id="login_back_button">
1263+ <property name="label">gtk-go-back</property>
1264+ <property name="visible">True</property>
1265+ <property name="can_focus">True</property>
1266+ <property name="receives_default">True</property>
1267+ <property name="use_stock">True</property>
1268+ <signal name="clicked" handler="on_login_back_button_clicked"/>
1269+ </object>
1270+ <packing>
1271+ <property name="expand">False</property>
1272+ <property name="fill">False</property>
1273+ <property name="position">1</property>
1274+ </packing>
1275+ </child>
1276+ <child>
1277+ <object class="GtkButton" id="login_ok_button">
1278+ <property name="label">gtk-connect</property>
1279+ <property name="visible">True</property>
1280+ <property name="can_focus">True</property>
1281+ <property name="receives_default">True</property>
1282+ <property name="use_stock">True</property>
1283+ <signal name="clicked" handler="on_login_connect_button_clicked"/>
1284+ </object>
1285+ <packing>
1286+ <property name="expand">False</property>
1287+ <property name="fill">False</property>
1288+ <property name="position">2</property>
1289+ </packing>
1290+ </child>
1291+ </object>
1292+ <packing>
1293+ <property name="expand">False</property>
1294+ <property name="position">1</property>
1295+ </packing>
1296+ </child>
1297+ </object>
1298+ <object class="GtkVBox" id="reset_password_vbox">
1299+ <property name="visible">True</property>
1300+ <property name="spacing">10</property>
1301+ <child>
1302+ <object class="GtkAlignment" id="alignment1">
1303+ <property name="visible">True</property>
1304+ <property name="xscale">0</property>
1305+ <property name="yscale">0</property>
1306+ <child>
1307+ <object class="GtkVBox" id="reset_password_details_vbox">
1308+ <property name="visible">True</property>
1309+ <property name="spacing">5</property>
1310+ <child>
1311+ <placeholder/>
1312+ </child>
1313+ <child>
1314+ <placeholder/>
1315+ </child>
1316+ <child>
1317+ <placeholder/>
1318+ </child>
1319+ </object>
1320+ </child>
1321+ </object>
1322+ <packing>
1323+ <property name="position">0</property>
1324+ </packing>
1325+ </child>
1326+ <child>
1327+ <object class="GtkHButtonBox" id="hbuttonbox6">
1328+ <property name="visible">True</property>
1329+ <property name="spacing">5</property>
1330+ <property name="layout_style">end</property>
1331+ <child>
1332+ <object class="GtkButton" id="reset_password_cancel_button">
1333+ <property name="label">gtk-cancel</property>
1334+ <property name="visible">True</property>
1335+ <property name="can_focus">True</property>
1336+ <property name="receives_default">True</property>
1337+ <property name="use_stock">True</property>
1338+ </object>
1339+ <packing>
1340+ <property name="expand">False</property>
1341+ <property name="fill">False</property>
1342+ <property name="position">0</property>
1343+ </packing>
1344+ </child>
1345+ <child>
1346+ <object class="GtkButton" id="reset_password_ok_button">
1347+ <property name="label">gtk-ok</property>
1348+ <property name="visible">True</property>
1349+ <property name="can_focus">True</property>
1350+ <property name="receives_default">True</property>
1351+ <property name="use_stock">True</property>
1352+ </object>
1353+ <packing>
1354+ <property name="expand">False</property>
1355+ <property name="fill">False</property>
1356+ <property name="position">1</property>
1357+ </packing>
1358+ </child>
1359+ </object>
1360+ <packing>
1361+ <property name="expand">False</property>
1362+ <property name="position">1</property>
1363+ </packing>
1364+ </child>
1365+ </object>
1366+ <object class="GtkVBox" id="reset_email_vbox">
1367+ <property name="visible">True</property>
1368+ <property name="spacing">10</property>
1369+ <child>
1370+ <object class="GtkAlignment" id="alignment2">
1371+ <property name="visible">True</property>
1372+ <property name="xscale">0</property>
1373+ <property name="yscale">0</property>
1374+ <child>
1375+ <object class="GtkVBox" id="reset_email_details_vbox">
1376+ <property name="visible">True</property>
1377+ <property name="spacing">5</property>
1378+ <child>
1379+ <placeholder/>
1380+ </child>
1381+ </object>
1382+ </child>
1383+ </object>
1384+ <packing>
1385+ <property name="position">0</property>
1386+ </packing>
1387+ </child>
1388+ <child>
1389+ <object class="GtkHButtonBox" id="hbuttonbox7">
1390+ <property name="visible">True</property>
1391+ <property name="spacing">5</property>
1392+ <property name="layout_style">end</property>
1393+ <child>
1394+ <object class="GtkButton" id="reset_email_cancel_button">
1395+ <property name="label">gtk-cancel</property>
1396+ <property name="visible">True</property>
1397+ <property name="can_focus">True</property>
1398+ <property name="receives_default">True</property>
1399+ <property name="use_stock">True</property>
1400+ </object>
1401+ <packing>
1402+ <property name="expand">False</property>
1403+ <property name="fill">False</property>
1404+ <property name="position">0</property>
1405+ </packing>
1406+ </child>
1407+ <child>
1408+ <object class="GtkButton" id="reset_email_back_button">
1409+ <property name="label">gtk-go-back</property>
1410+ <property name="visible">True</property>
1411+ <property name="can_focus">True</property>
1412+ <property name="receives_default">True</property>
1413+ <property name="use_stock">True</property>
1414+ <signal name="clicked" handler="on_reset_email_back_button_clicked"/>
1415+ </object>
1416+ <packing>
1417+ <property name="expand">False</property>
1418+ <property name="fill">False</property>
1419+ <property name="position">1</property>
1420+ </packing>
1421+ </child>
1422+ <child>
1423+ <object class="GtkButton" id="reset_email_ok_button">
1424+ <property name="label">gtk-ok</property>
1425+ <property name="visible">True</property>
1426+ <property name="can_focus">True</property>
1427+ <property name="receives_default">True</property>
1428+ <property name="use_stock">True</property>
1429+ <signal name="clicked" handler="on_reset_email_ok_button_clicked"/>
1430+ </object>
1431+ <packing>
1432+ <property name="expand">False</property>
1433+ <property name="fill">False</property>
1434+ <property name="position">2</property>
1435+ </packing>
1436+ </child>
1437+ </object>
1438+ <packing>
1439+ <property name="expand">False</property>
1440+ <property name="position">1</property>
1441+ </packing>
1442+ </child>
1443+ </object>
1444+ <object class="GtkVBox" id="success_vbox">
1445+ <property name="visible">True</property>
1446+ <property name="spacing">10</property>
1447+ <child>
1448+ <object class="GtkLabel" id="success_label">
1449+ <property name="visible">True</property>
1450+ <property name="wrap">True</property>
1451+ </object>
1452+ <packing>
1453+ <property name="padding">10</property>
1454+ <property name="position">0</property>
1455+ </packing>
1456+ </child>
1457+ <child>
1458+ <object class="GtkHButtonBox" id="hbuttonbox8">
1459+ <property name="visible">True</property>
1460+ <property name="layout_style">end</property>
1461+ <child>
1462+ <object class="GtkButton" id="success_close_button">
1463+ <property name="label">gtk-close</property>
1464+ <property name="visible">True</property>
1465+ <property name="can_focus">True</property>
1466+ <property name="receives_default">True</property>
1467+ <property name="use_stock">True</property>
1468+ <signal name="clicked" handler="on_close_clicked"/>
1469+ </object>
1470+ <packing>
1471+ <property name="expand">False</property>
1472+ <property name="fill">False</property>
1473+ <property name="position">0</property>
1474+ </packing>
1475+ </child>
1476+ </object>
1477+ <packing>
1478+ <property name="expand">False</property>
1479+ <property name="position">1</property>
1480+ </packing>
1481+ </child>
1482+ </object>
1483+</interface>
1484
1485=== modified file 'debian/changelog'
1486--- debian/changelog 2010-06-16 15:11:04 +0000
1487+++ debian/changelog 2010-08-11 18:19:01 +0000
1488@@ -1,3 +1,10 @@
1489+ubuntu-sso-client (0.98-0ubuntu1) UNRELEASED; urgency=low
1490+
1491+ * New upstream release.
1492+ * Switch to dpkg-source 3.0 (quilt) format
1493+
1494+ -- Natalia Bidart (nessita) <natalia.bidart@gmail.com> Wed, 11 Aug 2010 15:09:32 -0300
1495+
1496 ubuntu-sso-client (0.0.3-0ubuntu1) maverick; urgency=low
1497
1498 * New upstream release.
1499
1500=== modified file 'debian/control'
1501--- debian/control 2010-06-16 15:11:04 +0000
1502+++ debian/control 2010-08-11 18:19:01 +0000
1503@@ -14,7 +14,16 @@
1504 Architecture: all
1505 XB-Python-Version: ${python:Versions}
1506 Depends: ${misc:Depends},
1507- ${python:Depends}
1508+ ${python:Depends},
1509+ python-dbus,
1510+ python-gnomekeyring,
1511+ python-gtk2,
1512+ python-lazr.restfulclient,
1513+ python-oauth,
1514+ python-twisted-core,
1515+ python-twisted-web,
1516+ python-webkit,
1517+ python-xdg
1518 Description: Ubuntu Single Sign-On client
1519 Desktop service to allow applications to sign into Ubuntu services via
1520 SSO
1521
1522=== added directory 'debian/source'
1523=== added file 'debian/source/format'
1524--- debian/source/format 1970-01-01 00:00:00 +0000
1525+++ debian/source/format 2010-08-11 18:19:01 +0000
1526@@ -0,0 +1,1 @@
1527+3.0 (quilt)
1528
1529=== modified file 'run-tests'
1530--- run-tests 2010-06-16 15:11:04 +0000
1531+++ run-tests 2010-08-11 18:19:01 +0000
1532@@ -15,4 +15,12 @@
1533 # You should have received a copy of the GNU General Public License along
1534 # with this program. If not, see <http://www.gnu.org/licenses/>.
1535
1536-trial ubuntu_sso
1537+`which xvfb-run` ./contrib/test $@
1538+pyflakes bin ubuntu_sso
1539+if [ -x `which pep8` ]; then
1540+ pep8 --repeat bin ubuntu_sso/gui.py ubuntu_sso/main.py \
1541+ ubuntu_sso/tests/test_gui.py ubuntu_sso/tests/test_main.py contrib/
1542+else
1543+ echo "Please install the 'pep8' package."
1544+fi
1545+rm -rf _trial_temp/
1546\ No newline at end of file
1547
1548=== modified file 'setup.py'
1549--- setup.py 2010-06-16 15:11:04 +0000
1550+++ setup.py 2010-08-11 18:19:01 +0000
1551@@ -71,7 +71,7 @@
1552
1553 setup(
1554 name='ubuntu-sso-client',
1555- version='0.0.3',
1556+ version='0.98',
1557 license='GPL v3',
1558 author='Natalia Bidart',
1559 author_email='natalia.bidart@canonical.com',
1560@@ -80,15 +80,14 @@
1561 'to Ubuntu services via SSO',
1562 url='https://launchpad.net/ubuntu-sso-client',
1563 packages=['ubuntu_sso'],
1564- data_files=[('share/dbus-1/services',
1565- ['data/com.ubuntu.sso.service',]),
1566- ('/etc/xdg/ubuntu-sso/',
1567- ['data/oauth_urls',]),
1568- ('/etc/xdg/ubuntu-sso/oauth_registration.d',
1569- ['data/oauth_registration.d/ubuntuone',]),
1570- ('lib/ubuntu-sso-client',
1571- ['bin/ubuntu-sso-login',]),
1572- ],
1573+ data_files=[
1574+ ('share/dbus-1/services', ['data/com.ubuntu.sso.service',]),
1575+ ('lib/ubuntu-sso-client', ['bin/ubuntu-sso-login',]),
1576+ ('share/ubuntu-sso-client/data', ['data/ui.glade', 'data/email.png',]),
1577+ ('/etc/xdg/ubuntu-sso/', ['data/oauth_urls',]),
1578+ ('/etc/xdg/ubuntu-sso/oauth_registration.d',
1579+ ['data/oauth_registration.d/ubuntuone',]),
1580+ ],
1581
1582 cmdclass = {
1583 'build' : SSOBuild,
1584
1585=== modified file 'ubuntu_sso/__init__.py'
1586--- ubuntu_sso/__init__.py 2010-06-16 15:11:04 +0000
1587+++ ubuntu_sso/__init__.py 2010-08-11 18:19:01 +0000
1588@@ -16,5 +16,10 @@
1589 """OAuth client authorisation code."""
1590
1591 # constants
1592+DBUS_PATH_AUTH = "/"
1593+DBUS_BUS_NAME = "com.ubuntu.sso"
1594+DBUS_PATH = "/sso"
1595+DBUS_CRED_PATH = "/credentials"
1596 DBUS_IFACE_AUTH_NAME = "com.ubuntu.sso"
1597-DBUS_PATH_AUTH = "/"
1598+DBUS_IFACE_USER_NAME = "com.ubuntu.sso.UserManagement"
1599+DBUS_IFACE_CRED_NAME = "com.ubuntu.sso.ApplicationCredentials"
1600
1601=== modified file 'ubuntu_sso/auth.py'
1602--- ubuntu_sso/auth.py 2010-06-16 15:11:04 +0000
1603+++ ubuntu_sso/auth.py 2010-08-11 18:19:01 +0000
1604@@ -23,11 +23,12 @@
1605
1606 __metaclass__ = type
1607
1608-import subprocess
1609-import random
1610 import dbus
1611 import os
1612+import random
1613+import subprocess
1614 import socket, httplib, urllib
1615+import sys
1616
1617 import gnomekeyring
1618 from oauth import oauth
1619@@ -231,13 +232,12 @@
1620
1621 def make_token_request(self, oauth_request):
1622 """Perform the given `OAuthRequest` and return the associated token."""
1623-
1624- logger.debug("Making a token request")
1625+ logger.debug("Making a token request.")
1626 # Note that we monkeypatched httplib above to handle invalid certs
1627 # Ways this urlopen can fail:
1628 # bad certificate
1629 # raises IOError, e.args[1] == SSLError, e.args[1].errno == 1
1630- # No such server
1631+ # No such server
1632 # raises IOError, e.args[1] == SSLError, e.args[1].errno == -2
1633 try:
1634 opener = FancyURLOpenerWithRedirectedPOST()
1635@@ -245,19 +245,19 @@
1636 data = fp.read()
1637 except IOError, e:
1638 self._forward_error_callback(e)
1639+ logger.exception("make_token_request failed when getting a token.")
1640 return
1641-
1642+
1643 # we deliberately trap anything that might go wrong when parsing the
1644 # token, because we do not want this to explicitly fail
1645 # pylint: disable-msg=W0702
1646 try:
1647 out_token = oauth.OAuthToken.from_string(data)
1648- logger.debug("Token successfully requested")
1649+ logger.info("Token successfully requested")
1650 return out_token
1651 except:
1652 error = Exception(data)
1653- logger.error("Token was not successfully retrieved: data was '%s'",
1654- str(error))
1655+ logger.exception("Token was not retrieved: data was '%s'", data)
1656 self._forward_error_callback(error)
1657
1658 def open_in_browser(self, url):
1659@@ -358,6 +358,13 @@
1660 logger.debug("Making token request")
1661 self.request_token = self.make_token_request(oauth_request)
1662
1663+ if self.request_token is None:
1664+ msg = "acquire_access_token: request_token is None after " \
1665+ "make_token_request. Stopping reactor and exiting."
1666+ logger.error(msg)
1667+ reactor.stop()
1668+ sys.exit(1)
1669+
1670 # Request authorisation from the user
1671 oauth_request = oauth.OAuthRequest.from_token_and_callback(
1672 http_url=self.user_authorisation_url,
1673@@ -416,9 +423,7 @@
1674 self.callback_parent(access_token)
1675 except NoAccessToken:
1676 if self.do_login:
1677- access_token = self.acquire_access_token_if_online(
1678- description,
1679- store=True)
1680+ self.acquire_access_token_if_online(description, store=True)
1681 else:
1682 if self.callback_notoken is not None:
1683 self.callback_notoken()
1684
1685=== added file 'ubuntu_sso/gui.py'
1686--- ubuntu_sso/gui.py 1970-01-01 00:00:00 +0000
1687+++ ubuntu_sso/gui.py 2010-08-11 18:19:01 +0000
1688@@ -0,0 +1,797 @@
1689+# ubuntu_sso.gui - GUI for login and registration
1690+#
1691+# Author: Natalia Bidart <natalia.bidart@canonical.com>
1692+#
1693+# Copyright 2010 Canonical Ltd.
1694+#
1695+# This program is free software: you can redistribute it and/or modify it
1696+# under the terms of the GNU General Public License version 3, as published
1697+# by the Free Software Foundation.
1698+#
1699+# This program is distributed in the hope that it will be useful, but
1700+# WITHOUT ANY WARRANTY; without even the implied warranties of
1701+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
1702+# PURPOSE. See the GNU General Public License for more details.
1703+#
1704+# You should have received a copy of the GNU General Public License along
1705+# with this program. If not, see <http://www.gnu.org/licenses/>.
1706+
1707+"""User registration GUI."""
1708+
1709+import logging
1710+import os
1711+import re
1712+import tempfile
1713+
1714+from functools import wraps
1715+
1716+import dbus
1717+import gettext
1718+import gobject
1719+import gtk
1720+import webkit
1721+import xdg
1722+
1723+from dbus.mainloop.glib import DBusGMainLoop
1724+
1725+from ubuntu_sso import DBUS_PATH, DBUS_BUS_NAME, DBUS_IFACE_USER_NAME
1726+from ubuntu_sso.logger import setupLogging
1727+
1728+
1729+_ = gettext.gettext
1730+DBusGMainLoop(set_as_default=True)
1731+logger = setupLogging('ubuntu_sso.gui')
1732+
1733+
1734+NO_OP = lambda *args, **kwargs: None
1735+DEFAULT_WIDTH = 30
1736+# XXX: to be replaced by values from the theme
1737+HELP_TEXT_COLOR = gtk.gdk.Color("#bfbfbf")
1738+WARNING_TEXT_COLOR = gtk.gdk.Color("red")
1739+
1740+SIG_LOGIN_FAILED = 'login-failed'
1741+SIG_LOGIN_SUCCEEDED = 'login-succeeded'
1742+SIG_REGISTRATION_FAILED = 'registration-failed'
1743+SIG_REGISTRATION_SUCCEEDED = 'registration-succeeded'
1744+SIG_USER_CANCELATION = 'user-cancelation'
1745+
1746+SIGNAL_ARGUMENTS = [
1747+ (SIG_LOGIN_FAILED, ()),
1748+ (SIG_LOGIN_SUCCEEDED, (gobject.TYPE_STRING,)),
1749+ (SIG_REGISTRATION_FAILED, ()),
1750+ (SIG_REGISTRATION_SUCCEEDED, (gobject.TYPE_STRING,)),
1751+ (SIG_USER_CANCELATION, ()),
1752+]
1753+
1754+for sig, args in SIGNAL_ARGUMENTS:
1755+ gobject.signal_new(sig, gtk.Window, gobject.SIGNAL_RUN_FIRST,
1756+ gobject.TYPE_NONE, args)
1757+
1758+
1759+def get_data_dir():
1760+ """Build absolute path to the 'data' directory."""
1761+ module = os.path.dirname(__file__)
1762+ result = os.path.abspath(os.path.join(module, os.pardir, 'data'))
1763+ logger.debug('get_data_file: trying to load from %r (exists? %s)',
1764+ result, os.path.exists(result))
1765+ if os.path.exists(result):
1766+ logger.info('get_data_file: returning data dir located at %r.', result)
1767+ return result
1768+
1769+ # no local data dir, looking within system data dirs
1770+ data_dirs = xdg.BaseDirectory.xdg_data_dirs
1771+ for path in data_dirs:
1772+ result = os.path.join(path, 'ubuntu-sso-client', 'data')
1773+ result = os.path.abspath(result)
1774+ logger.debug('get_data_file: trying to load from %r (exists? %s)',
1775+ result, os.path.exists(result))
1776+ if os.path.exists(result):
1777+ logger.info('get_data_file: data dir located at %r.', result)
1778+ return result
1779+ else:
1780+ msg = 'get_data_file: can not build a valid data path. Giving up.' \
1781+ '__file__ is %r, data_dirs are %r'
1782+ logger.error(msg, __file__, data_dirs)
1783+
1784+
1785+def get_data_file(filename):
1786+ """Build absolute path to 'filename' within the 'data' directory."""
1787+ return os.path.join(get_data_dir(), filename)
1788+
1789+
1790+class LabeledEntry(gtk.Entry):
1791+ """An entry that displays the label within itself ina grey color."""
1792+
1793+ def __init__(self, label, is_password=False, *args, **kwargs):
1794+ self.label = label
1795+ self.is_password = is_password
1796+ self._user_input = None
1797+
1798+ super(LabeledEntry, self).__init__(*args, **kwargs)
1799+
1800+ self.set_width_chars(DEFAULT_WIDTH)
1801+ self._set_label(self, None)
1802+ self.set_tooltip_text(self.label)
1803+ self.connect('focus-in-event', self._clear_text)
1804+ self.connect('focus-out-event', self._set_label)
1805+ self.show()
1806+
1807+ def _clear_text(self, *args, **kwargs):
1808+ """Clear text and restore text color."""
1809+ self.set_text(self.get_text())
1810+
1811+ self.modify_text(gtk.STATE_NORMAL, None) # restore to theme's default
1812+
1813+ if self.is_password:
1814+ self.set_visibility(False)
1815+
1816+ return False # propagate the event further
1817+
1818+ def _set_label(self, *args, **kwargs):
1819+ """Set the proper label and proper coloring."""
1820+ if self.get_text():
1821+ return
1822+
1823+ self.set_text(self.label)
1824+ self.modify_text(gtk.STATE_NORMAL, HELP_TEXT_COLOR)
1825+
1826+ if self.is_password:
1827+ self.set_visibility(True)
1828+
1829+ return False # propagate the event further
1830+
1831+ def get_text(self):
1832+ """Get text only if it's not the label nor empty."""
1833+ result = super(LabeledEntry, self).get_text()
1834+ if result == self.label or result.isspace():
1835+ result = ''
1836+ return result
1837+
1838+
1839+class UbuntuSSOClientGUI(object):
1840+ """Ubuntu single sign on GUI."""
1841+
1842+ CAPTCHA_SOLUTION_ENTRY = _('Type the characters above')
1843+ CONNECT_HELP_LABEL = _('To connect this computer to') + ' %s ' + \
1844+ _('enter your details below.')
1845+ EMAIL1_ENTRY = _('Email address')
1846+ EMAIL2_ENTRY = _('Re-type Email address')
1847+ EMAIL_MISMATCH = _('The email addresses don\'t match, please double check '
1848+ 'and try entering them again.')
1849+ EMAIL_INVALID = _('The email must be a valid email address.')
1850+ EMAIL_TOKEN_ENTRY = _('Enter code verification here')
1851+ FIELD_REQUIRED = _('This field is required.')
1852+ FORGOTTEN_PASSWORD_BUTTON = _('I\'ve forgotten my password')
1853+ JOIN_HEADER_LABEL = _('Create') + ' %s ' + _('account')
1854+ LOADING = _('Loading...')
1855+ LOGIN_BUTTON_LABEL = _('Already have an account? Click here to sign in')
1856+ LOGIN_EMAIL_ENTRY = _('Email address')
1857+ LOGIN_HEADER_LABEL = _('Connect to') + ' %s'
1858+ LOGIN_PASSWORD_ENTRY = _('Password')
1859+ NAME_ENTRY = _('Name')
1860+ NEXT = _('Next')
1861+ ONE_MOMENT_PLEASE = _('One moment please...')
1862+ PASSWORD1_ENTRY = RESET_PASSWORD1_ENTRY = _('Password')
1863+ PASSWORD2_ENTRY = RESET_PASSWORD2_ENTRY = _('Re-type Password')
1864+ PASSWORD_HELP = _('The password must have a minimum of 8 characters and '
1865+ 'include one uppercase character and one number.')
1866+ PASSWORD_MISMATCH = _('The passwords don\'t match, please double check '
1867+ 'and try entering them again.')
1868+ PASSWORD_TOO_WEAK = _('The password is too weak.')
1869+ RESET_CODE_ENTRY = _('Reset code')
1870+ RESET_EMAIL_ENTRY = _('Email address')
1871+ RESET_EMAIL_LABEL = _('To reset your password enter your email address '\
1872+ 'below:')
1873+ RESET_PASSWORD = _('Reset Password')
1874+ RESET_PASSWORD_LABEL = _('A password reset code has been sent to %s. ' \
1875+ 'Please enter the code below along with your ' \
1876+ 'new password.')
1877+ SUCCESS = _('The process finished successfully. Congratulations!')
1878+ TC = _('Terms & Conditions')
1879+ TC_NOT_ACCEPTED = _('Agreeing to the Ubuntu One Terms & Conditions is '
1880+ 'required to subscribe.')
1881+ UNKNOWN_ERROR = _('There was an error when trying to complete the ' \
1882+ 'process. Please check the information and try again.')
1883+ VERIFY_EMAIL_LABEL = _("""Enter verification code.
1884+
1885+A verification code has just been sent to your email address.
1886+Please enter this code below.""")
1887+ YES_TO_TC = _('I agree with the')
1888+ YES_TO_UPDATES = _('Yes! Email me tips and updates.')
1889+
1890+ def __init__(self, app_name, tc_uri, help_text,
1891+ window_id=0, close_callback=None):
1892+ """Create the GUI and initialize widgets."""
1893+ gtk.link_button_set_uri_hook(NO_OP)
1894+
1895+ self._captcha_filename = tempfile.mktemp()
1896+ self._captcha_id = None
1897+ self._signals_receivers = {}
1898+
1899+ self.app_name = app_name
1900+ self.app_label = '<b>%s</b>' % app_name
1901+ self.tc_uri = tc_uri
1902+ self.help_text = help_text
1903+ self.close_callback = close_callback
1904+
1905+ ui_filename = get_data_file('ui.glade')
1906+ builder = gtk.Builder()
1907+ builder.add_from_file(ui_filename)
1908+ builder.connect_signals(self)
1909+
1910+ # XXX: every button should have tests for 'activate' signal
1911+
1912+ self.widgets = []
1913+ self.warnings = []
1914+ self.cancels = []
1915+ for obj in builder.get_objects():
1916+ name = getattr(obj, 'name', None)
1917+ if name is None and isinstance(obj, gtk.Buildable):
1918+ # work around bug lp:507739
1919+ name = gtk.Buildable.get_name(obj)
1920+ if name is None:
1921+ logging.warn("%s has no name (??)", obj)
1922+ else:
1923+ self.widgets.append(name)
1924+ setattr(self, name, obj)
1925+ if 'warning' in name:
1926+ self.warnings.append(obj)
1927+ obj.hide()
1928+ if 'cancel_button' in name:
1929+ obj.connect('clicked', self.on_close_clicked)
1930+ self.cancels.append(obj)
1931+
1932+ self.entries = ('name_entry', 'email1_entry', 'email2_entry',
1933+ 'password1_entry', 'password2_entry',
1934+ 'captcha_solution_entry', 'email_token_entry',
1935+ 'login_email_entry', 'login_password_entry',
1936+ 'reset_email_entry', 'reset_code_entry',
1937+ 'reset_password1_entry', 'reset_password2_entry')
1938+
1939+ for name in self.entries:
1940+ label = getattr(self, name.upper())
1941+ is_password = 'password' in name
1942+ entry = LabeledEntry(label=label, is_password=is_password)
1943+ setattr(self, name, entry)
1944+ assert getattr(self, name) is not None
1945+
1946+ self.window.set_icon_name('ubuntu-logo')
1947+
1948+ self.bus = dbus.SessionBus()
1949+ self.bus.add_signal_receiver = self._log(self.bus.add_signal_receiver)
1950+ obj = self.bus.get_object(bus_name=DBUS_BUS_NAME,
1951+ object_path=DBUS_PATH,
1952+ follow_name_owner_changes=True)
1953+ self.iface_name = DBUS_IFACE_USER_NAME
1954+ self.backend = dbus.Interface(object=obj,
1955+ dbus_interface=self.iface_name)
1956+ logger.debug('UbuntuSSOClientGUI: backend created: %r', self.backend)
1957+
1958+ self.pages = (self.enter_details_vbox, self.processing_vbox,
1959+ self.verify_email_vbox, self.success_vbox,
1960+ self.tc_browser_vbox, self.login_vbox,
1961+ self.reset_email_vbox, self.reset_password_vbox)
1962+
1963+ self._append_page(self._build_enter_details_page())
1964+ self._append_page(self._build_tc_page())
1965+ self._append_page(self._build_processing_page())
1966+ self._append_page(self._build_verify_email_page())
1967+ self._append_page(self._build_success_page())
1968+ self._append_page(self._build_login_page())
1969+ self._append_page(self._build_reset_email_page())
1970+ self._append_page(self._build_reset_password_page())
1971+
1972+ self.login_button.grab_focus()
1973+ self._set_current_page(self.enter_details_vbox)
1974+ self._setup_signals()
1975+
1976+ if window_id != 0:
1977+ # be as robust as possible:
1978+ # if the window_id is not "good", set_transient_for will fail
1979+ # awfully, and we don't want that: if the window_id is bad we can
1980+ # still do everything as a standalone window. Also,
1981+ # window_foreign_new may return None breaking set_transient_for.
1982+ try:
1983+ r = gtk.gdk.window_foreign_new(window_id)
1984+ self.window.realize()
1985+ self.window.window.set_transient_for(r)
1986+ except:
1987+ msg = 'UbuntuSSOClientGUI: failed set_transient_for win id %r'
1988+ logger.exception(msg, window_id)
1989+ self.window.show()
1990+
1991+ def _setup_signals(self):
1992+ """Bind signals to callbacks to be able to test the pages."""
1993+ iface = self.iface_name
1994+ self._signals = [
1995+ (iface, 'CaptchaGenerated', self.on_captcha_generated),
1996+ (iface, 'CaptchaGenerationError',
1997+ self.on_captcha_generation_error),
1998+ (iface, 'UserRegistered', self.on_user_registered),
1999+ (iface, 'UserRegistrationError', self.on_user_registration_error),
2000+ (iface, 'EmailValidated', self.on_email_validated),
2001+ (iface, 'EmailValidationError', self.on_email_validation_error),
2002+ (iface, 'LoggedIn', self.on_logged_in),
2003+ (iface, 'LoginError', self.on_login_error),
2004+ ]
2005+ for iface, signal, method in self._signals:
2006+ self.bus.add_signal_receiver(method, signal_name=signal,
2007+ dbus_interface=iface)
2008+
2009+ def _debug(self, *args, **kwargs):
2010+ """Do some debugging."""
2011+ print args, kwargs
2012+
2013+ def _log(self, f):
2014+ """Keep track of added signals to the internal bus."""
2015+
2016+ @wraps(f)
2017+ def inner(method, signal_name, dbus_interface):
2018+ """Do the deed."""
2019+ actual = self._signals_receivers.get((dbus_interface, signal_name))
2020+ assert actual is None
2021+ f(method, signal_name, dbus_interface)
2022+ self._signals_receivers[(dbus_interface, signal_name)] = method
2023+
2024+ return inner
2025+
2026+ def _add_spinner_to_container(self, container, legend=None):
2027+ """Add a spinner to 'container'."""
2028+ spinner = gtk.Spinner()
2029+ spinner.start()
2030+
2031+ label = gtk.Label()
2032+ if legend:
2033+ label.set_text(legend)
2034+ else:
2035+ label.set_text(self.LOADING)
2036+
2037+ hbox = gtk.HBox(spacing=5)
2038+ hbox.pack_start(spinner, expand=False)
2039+ hbox.pack_start(label, expand=False)
2040+
2041+ alignment = gtk.Alignment(xalign=0.5, yalign=0.5)
2042+ alignment.add(hbox)
2043+ alignment.show_all()
2044+
2045+ container.add(alignment)
2046+
2047+ # helpers
2048+
2049+ def _set_warning_message(self, widget, message):
2050+ """Set 'message' as text for 'widget'."""
2051+ widget.set_text(message)
2052+ widget.modify_fg(gtk.STATE_NORMAL, WARNING_TEXT_COLOR)
2053+ widget.show()
2054+
2055+ def _clear_warnings(self):
2056+ """Clear all warning messages."""
2057+ for widget in self.warnings:
2058+ widget.set_text('')
2059+ widget.hide()
2060+
2061+ def _non_empty_input(self, widget):
2062+ """Return weather widget has non empty content."""
2063+ text = widget.get_text()
2064+ return bool(text and not text.isspace())
2065+
2066+ # build pages
2067+
2068+ def _append_page(self, page):
2069+ """Append 'page' to the 'window'."""
2070+ self.window.get_children()[0].pack_start(page)
2071+
2072+ def _set_header(self, header):
2073+ """Set 'header' as the window title and header."""
2074+ markup = '<span size="xx-large">%s</span>'
2075+ self.header_label.set_markup(markup % header)
2076+ self.window.set_title(self.header_label.get_text()) # avoid markup
2077+
2078+ def _set_current_page(self, current_page, warning_text=None):
2079+ """Hide all the pages but 'current_page'."""
2080+ if hasattr(current_page, 'header'):
2081+ self._set_header(current_page.header)
2082+
2083+ if hasattr(current_page, 'help_text'):
2084+ self.help_label.set_markup(current_page.help_text)
2085+
2086+ if warning_text is not None:
2087+ self._set_warning_message(self.warning_label, warning_text)
2088+ else:
2089+ self.warning_label.hide()
2090+
2091+ for page in self.pages:
2092+ if page is current_page:
2093+ page.show()
2094+ else:
2095+ page.hide()
2096+
2097+ def _set_captcha_loading(self):
2098+ """Present a spinner to the user while the captcha is downloaded."""
2099+ self.captcha_image.hide()
2100+ self._add_spinner_to_container(self.captcha_loading)
2101+ white = gtk.gdk.Color('white')
2102+ self.captcha_loading.modify_bg(gtk.STATE_NORMAL, white)
2103+ self.captcha_loading.show_all()
2104+ self.join_ok_button.set_sensitive(False)
2105+
2106+ def _set_captcha_image(self):
2107+ """Present a captcha image to the user to be resolved."""
2108+ self.captcha_loading.hide()
2109+ self.join_ok_button.set_sensitive(True)
2110+ self.captcha_image.set_from_file(self._captcha_filename)
2111+ self.captcha_image.show()
2112+
2113+ def _build_enter_details_page(self):
2114+ """Build the enter details page."""
2115+ self.enter_details_vbox.header = (self.JOIN_HEADER_LABEL %
2116+ self.app_label)
2117+ self.enter_details_vbox.help_text = self.help_text
2118+
2119+ self.enter_details_vbox.pack_start(self.name_entry, expand=False)
2120+ self.enter_details_vbox.reorder_child(self.name_entry, 0)
2121+ self.captcha_solution_vbox.pack_start(self.captcha_solution_entry,
2122+ expand=False)
2123+ self.captcha_solution_vbox.reorder_child(self.captcha_solution_entry,
2124+ 0)
2125+
2126+ self.emails_hbox.pack_start(self.email1_entry)
2127+ self.emails_hbox.pack_start(self.email2_entry)
2128+
2129+ self.passwords_hbox.pack_start(self.password1_entry)
2130+ self.passwords_hbox.pack_start(self.password2_entry)
2131+ help_msg = '<small>%s</small>' % self.PASSWORD_HELP
2132+ self.password_help_label.set_markup(help_msg)
2133+
2134+ if not os.path.exists(self._captcha_filename):
2135+ self._set_captcha_loading()
2136+ else:
2137+ self._set_captcha_image()
2138+
2139+ self.yes_to_updates_checkbutton.set_label(self.YES_TO_UPDATES)
2140+ self.yes_to_tc_checkbutton.set_label(self.YES_TO_TC)
2141+ self.tc_button.set_label(self.TC)
2142+ self.login_button.set_label(self.LOGIN_BUTTON_LABEL)
2143+
2144+ logger.info('Calling generate_captcha with filename path at %r',
2145+ self._captcha_filename)
2146+ f = self.backend.generate_captcha
2147+ f(self._captcha_filename,
2148+ reply_handler=NO_OP, error_handler=NO_OP)
2149+
2150+ return self.enter_details_vbox
2151+
2152+ def _build_tc_page(self):
2153+ """Build the Terms & Conditions page."""
2154+ self.tc_browser = webkit.WebView()
2155+ self.tc_browser.open(self.tc_uri)
2156+ self.tc_browser.show()
2157+ self.tc_browser_window.add(self.tc_browser)
2158+
2159+ return self.tc_browser_vbox
2160+
2161+ def _build_processing_page(self):
2162+ """Build the processing page with a spinner."""
2163+ self._add_spinner_to_container(self.processing_vbox,
2164+ legend=self.ONE_MOMENT_PLEASE)
2165+ return self.processing_vbox
2166+
2167+ def _build_verify_email_page(self):
2168+ """Build the verify email page."""
2169+ self.verify_email_vbox.help_text = self.VERIFY_EMAIL_LABEL
2170+ self.verify_email_vbox.pack_start(self.email_token_entry, expand=False)
2171+ self.verify_email_vbox.reorder_child(self.email_token_entry, 1)
2172+
2173+ fname = get_data_file('email.png')
2174+ self._email_example_pixbuf = gtk.gdk.pixbuf_new_from_file(fname)
2175+ self.verify_email_image.set_from_pixbuf(self._email_example_pixbuf)
2176+
2177+ return self.verify_email_vbox
2178+
2179+ def _build_success_page(self):
2180+ """Build the success page."""
2181+ #self.success_vbox.help_text = ''
2182+ self.success_label.set_markup('<span size="x-large">%s</span>' %
2183+ self.SUCCESS)
2184+ return self.success_vbox
2185+
2186+ def _build_login_page(self):
2187+ """Build the login page."""
2188+ self.login_vbox.header = self.LOGIN_HEADER_LABEL % self.app_label
2189+ self.login_vbox.help_text = self.CONNECT_HELP_LABEL % self.app_label
2190+
2191+ self.login_details_vbox.pack_start(self.login_email_entry)
2192+ self.login_details_vbox.reorder_child(self.login_email_entry, 0)
2193+ self.login_details_vbox.pack_start(self.login_password_entry)
2194+ self.login_details_vbox.reorder_child(self.login_password_entry, 2)
2195+
2196+ msg = self.FORGOTTEN_PASSWORD_BUTTON
2197+ self.forgotten_password_button.set_label(msg)
2198+ self.login_ok_button.grab_focus()
2199+
2200+ return self.login_vbox
2201+
2202+ def _build_reset_email_page(self):
2203+ """Build the login page."""
2204+ self.reset_email_vbox.header = self.RESET_PASSWORD
2205+ self.reset_email_vbox.help_text = self.RESET_EMAIL_LABEL
2206+
2207+ self.reset_email_details_vbox.pack_start(self.reset_email_entry)
2208+ self.reset_email_entry.connect('changed',
2209+ self.on_reset_email_entry_changed)
2210+ self.reset_email_ok_button.set_label(self.NEXT)
2211+ self.reset_email_ok_button.set_sensitive(False)
2212+
2213+ return self.reset_email_vbox
2214+
2215+ def _build_reset_password_page(self):
2216+ """Build the login page."""
2217+ self.reset_password_vbox.header = self.RESET_PASSWORD
2218+ self.reset_password_vbox.help_text = self.RESET_PASSWORD_LABEL
2219+
2220+ self.reset_password_details_vbox.pack_start(self.reset_code_entry)
2221+ self.reset_password_details_vbox.pack_start(self.reset_password1_entry)
2222+ self.reset_password_details_vbox.pack_start(self.reset_password2_entry)
2223+
2224+ self.reset_code_entry.connect('changed',
2225+ self.on_reset_password_entry_changed)
2226+ self.reset_password1_entry.connect('changed',
2227+ self.on_reset_password_entry_changed)
2228+ self.reset_password2_entry.connect('changed',
2229+ self.on_reset_password_entry_changed)
2230+
2231+ self.reset_password_ok_button.set_label(self.RESET_PASSWORD)
2232+ self.reset_password_ok_button.set_sensitive(False)
2233+
2234+ return self.reset_password_vbox
2235+
2236+ # GTK callbacks
2237+
2238+ def run(self):
2239+ """Run the application."""
2240+ gtk.main()
2241+
2242+ def connect(self, signal_name, handler):
2243+ """Connect 'signal_name' with 'handler'."""
2244+ self.window.connect(signal_name, handler)
2245+
2246+ def on_close_clicked(self, *args, **kwargs):
2247+ """Call self.close_callback if defined."""
2248+ if os.path.exists(self._captcha_filename):
2249+ os.remove(self._captcha_filename)
2250+
2251+ # remove the signals from DBus
2252+ remove = self.bus.remove_signal_receiver
2253+ for (iface, signal) in self._signals_receivers.keys():
2254+ method = self._signals_receivers.pop((iface, signal))
2255+ remove(method, signal_name=signal, dbus_interface=iface)
2256+
2257+ # destroy main window
2258+ if self.window is not None:
2259+ self.window.hide()
2260+
2261+ if len(args) > 0 and args[0] in self.cancels:
2262+ self.window.emit(SIG_USER_CANCELATION)
2263+
2264+ # call user defined callback
2265+ if self.close_callback is not None:
2266+ logger.info('Calling custom close_callback %r with params %r, %r',
2267+ self.close_callback, args, kwargs)
2268+ self.close_callback(*args, **kwargs)
2269+
2270+ def on_sign_in_button_clicked(self, *args, **kwargs):
2271+ """User wants to sign in, present the Login page."""
2272+ self._set_current_page(self.login_vbox)
2273+
2274+ def on_join_ok_button_clicked(self, *args, **kwargs):
2275+ """Submit info for processing, present the processing vbox."""
2276+ self._clear_warnings()
2277+
2278+ error = False
2279+
2280+ name = self.name_entry.get_text()
2281+ if not name:
2282+ self._set_warning_message(self.name_warning_label,
2283+ self.FIELD_REQUIRED)
2284+ error |= True
2285+
2286+ # check email
2287+ email1 = self.email1_entry.get_text()
2288+ email2 = self.email2_entry.get_text()
2289+ if email1 != email2:
2290+ self._set_warning_message(self.email_warning_label,
2291+ self.EMAIL_MISMATCH)
2292+ error |= True
2293+
2294+ if '@' not in email1:
2295+ self._set_warning_message(self.email_warning_label,
2296+ self.EMAIL_INVALID)
2297+ error |= True
2298+
2299+ if not email1:
2300+ self._set_warning_message(self.email_warning_label,
2301+ self.FIELD_REQUIRED)
2302+ error |= True
2303+
2304+ # check password
2305+ password1 = self.password1_entry.get_text()
2306+ password2 = self.password2_entry.get_text()
2307+ if password1 != password2:
2308+ self._set_warning_message(self.password_warning_label,
2309+ self.PASSWORD_MISMATCH)
2310+ error |= True
2311+
2312+ if (len(password1) < 8 or
2313+ re.search('[A-Z]', password1) is None or
2314+ re.search('\d+', password1) is None):
2315+ self._set_warning_message(self.password_warning_label,
2316+ self.PASSWORD_TOO_WEAK)
2317+ error |= True
2318+
2319+ # check T&C
2320+ if not self.yes_to_tc_checkbutton.get_active():
2321+ self._set_warning_message(self.tc_warning_label,
2322+ self.TC_NOT_ACCEPTED)
2323+ error |= True
2324+
2325+ captcha_solution = self.captcha_solution_entry.get_text()
2326+ if not captcha_solution:
2327+ self._set_warning_message(self.captcha_solution_warning_label,
2328+ self.FIELD_REQUIRED)
2329+ error |= True
2330+
2331+ if error:
2332+ return
2333+
2334+ self._clear_warnings()
2335+ self._set_current_page(self.processing_vbox)
2336+
2337+ logger.info('Calling register_user with email %r, password <hidden>,' \
2338+ ' captcha_id %r and captcha_solution %r.', email1,
2339+ self._captcha_id, captcha_solution)
2340+ f = self.backend.register_user
2341+ f(email1, password1, self._captcha_id, captcha_solution,
2342+ reply_handler=NO_OP, error_handler=NO_OP)
2343+
2344+ def on_tc_button_clicked(self, *args, **kwargs):
2345+ """T & C button was clicked, show the browser with them."""
2346+ self._set_current_page(self.tc_browser_vbox)
2347+
2348+ def on_tc_back_button_clicked(self, *args, **kwargs):
2349+ """T & C 'back' button was clicked, return to the previous page."""
2350+ self._set_current_page(self.enter_details_vbox)
2351+
2352+ def on_verify_token_button_clicked(self, *args, **kwargs):
2353+ """The user entered the email token, let's verify!"""
2354+ email = self.email1_entry.get_text()
2355+ password = self.password1_entry.get_text()
2356+ email_token = self.email_token_entry.get_text()
2357+
2358+ logger.info('Calling validate_email with email %r, password <hidden>' \
2359+ ', app_name %r and email_token %r.', email,
2360+ self.app_name, email_token)
2361+ f = self.backend.validate_email
2362+ f(email, password, self.app_name, email_token,
2363+ reply_handler=NO_OP, error_handler=NO_OP)
2364+
2365+ self._set_current_page(self.processing_vbox)
2366+
2367+ def on_login_connect_button_clicked(self, *args, **kwargs):
2368+ """User wants to connect!"""
2369+ self._clear_warnings()
2370+
2371+ error = False
2372+ email = self.login_email_entry.get_text()
2373+
2374+ if '@' not in email:
2375+ self._set_warning_message(self.login_email_warning_label,
2376+ self.EMAIL_INVALID)
2377+ error |= True
2378+
2379+ if not email:
2380+ self._set_warning_message(self.login_email_warning_label,
2381+ self.FIELD_REQUIRED)
2382+ error |= True
2383+
2384+ password = self.login_password_entry.get_text()
2385+
2386+ if not password:
2387+ self._set_warning_message(self.login_password_warning_label,
2388+ self.FIELD_REQUIRED)
2389+ error |= True
2390+
2391+ if error:
2392+ return
2393+
2394+ self._clear_warnings()
2395+
2396+ f = self.backend.login
2397+ f(email, password, self.app_name,
2398+ reply_handler=NO_OP, error_handler=NO_OP)
2399+
2400+ self._set_current_page(self.processing_vbox)
2401+
2402+ def on_login_back_button_clicked(self, *args, **kwargs):
2403+ """User wants to go to the previous page."""
2404+ self._set_current_page(self.enter_details_vbox)
2405+
2406+ def on_forgotten_password_button_clicked(self, *args, **kwargs):
2407+ """User wants to reset the password."""
2408+ self._set_current_page(self.reset_email_vbox)
2409+
2410+ def on_reset_email_ok_button_clicked(self, *args, **kwargs):
2411+ """User entered the email address."""
2412+ email = self.reset_email_entry.get_text()
2413+ self.reset_password_vbox.help_text = self.RESET_PASSWORD_LABEL % email
2414+ self._set_current_page(self.reset_password_vbox)
2415+
2416+ def on_reset_email_back_button_clicked(self, *args, **kwargs):
2417+ """User wants to go to the previous page."""
2418+ self._set_current_page(self.login_vbox)
2419+
2420+ def on_reset_email_entry_changed(self, widget, *args, **kwargs):
2421+ """User is changing the 'widget' entry in the reset email page."""
2422+ self.reset_email_ok_button.set_sensitive(self._non_empty_input(widget))
2423+
2424+ def on_reset_password_entry_changed(self, *args, **kwargs):
2425+ """User is changing the 'widget' entry in the reset password page."""
2426+ sensitive = True
2427+ for entry in (self.reset_code_entry, self.reset_password1_entry,
2428+ self.reset_password2_entry):
2429+ sensitive &= self._non_empty_input(entry)
2430+ self.reset_password_ok_button.set_sensitive(sensitive)
2431+
2432+ # backend callbacks
2433+
2434+ def on_captcha_generated(self, captcha_id, *args, **kwargs):
2435+ """Captcha image has been generated and is available to be shown."""
2436+ assert captcha_id is not None
2437+ self._captcha_id = captcha_id
2438+ self._set_captcha_image()
2439+
2440+ def on_captcha_generation_error(self, *args, **kwargs):
2441+ """Captcha image generation failed."""
2442+ logger.warning('on_captcha_generation_error: args %r, kwargs %r',
2443+ args, kwargs)
2444+
2445+ def on_user_registered(self, *args, **kwargs):
2446+ """Registration can go on, user needs to verify email."""
2447+ logger.info('on_user_registered: user was successfully registered! ' \
2448+ 'args %r, kwargs %r.', args, kwargs)
2449+ self._set_current_page(self.verify_email_vbox)
2450+
2451+ def on_user_registration_error(self, *args, **kwargs):
2452+ """Captcha image generation failed."""
2453+ logger.warning('on_user_registration_error: args %r, kwargs %r',
2454+ args, kwargs)
2455+ self._set_current_page(self.enter_details_vbox,
2456+ warning_text=self.UNKNOWN_ERROR)
2457+ self.window.emit(SIG_REGISTRATION_FAILED)
2458+
2459+ def on_email_validated(self, app_name, *args, **kwargs):
2460+ """User email was successfully verified."""
2461+ logger.info('on_email_validated: email was successfully validated! ' \
2462+ 'app_name %r, args %r, kwargs %r.', app_name, args, kwargs)
2463+ self._set_current_page(self.success_vbox)
2464+ self.window.emit(SIG_REGISTRATION_SUCCEEDED, app_name)
2465+
2466+ def on_email_validation_error(self, *args, **kwargs):
2467+ """User email validation failed."""
2468+ logger.warning('on_email_validation_error: args %r, kwargs %r',
2469+ args, kwargs)
2470+ # XXX: show something to the user!!!
2471+ self.window.emit(SIG_REGISTRATION_FAILED)
2472+
2473+ def on_logged_in(self, app_name, *args, **kwargs):
2474+ """User was successfully logged in."""
2475+ logger.info('on_logged_in: user was successfully logged in! '
2476+ 'args %r, kwargs %r', args, kwargs)
2477+ self._set_current_page(self.success_vbox)
2478+ self.window.emit(SIG_LOGIN_SUCCEEDED, app_name)
2479+
2480+ def on_login_error(self, *args, **kwargs):
2481+ """User was not successfully logged in."""
2482+ logger.warning('on_login_error: args %r, kwargs %r', args, kwargs)
2483+ self._set_current_page(self.login_vbox,
2484+ warning_text=self.UNKNOWN_ERROR)
2485+ self.window.emit(SIG_LOGIN_FAILED)
2486
2487=== added file 'ubuntu_sso/keyring.py'
2488--- ubuntu_sso/keyring.py 1970-01-01 00:00:00 +0000
2489+++ ubuntu_sso/keyring.py 2010-08-11 18:19:01 +0000
2490@@ -0,0 +1,111 @@
2491+# Copyright (C) 2010 Canonical
2492+#
2493+# Authors:
2494+# Andrew Higginson
2495+#
2496+# This program is free software; you can redistribute it and/or modify it under
2497+# the terms of the GNU General Public License as published by the Free Software
2498+# Foundation; version 3.
2499+#
2500+# This program is distributed in the hope that it will be useful, but WITHOUT
2501+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
2502+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
2503+# details.
2504+#
2505+# You should have received a copy of the GNU General Public License along with
2506+# this program; if not, write to the Free Software Foundation, Inc.,
2507+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
2508+
2509+import gnomekeyring
2510+
2511+from urllib import urlencode
2512+from urlparse import parse_qsl
2513+
2514+class Keyring(object):
2515+
2516+ KEYRING_NAME = "login"
2517+
2518+ def __init__(self, app_name):
2519+
2520+ if not gnomekeyring.is_available():
2521+ raise gnomekeyring.NoKeyringDaemonError
2522+ self.app_name = app_name
2523+
2524+ def _create_keyring(self, name):
2525+ """
2526+ Creates a keyring, if it already exists,
2527+ it does nothing
2528+ """
2529+ keyring_names = gnomekeyring.list_keyring_names_sync()
2530+ if not name in keyring_names:
2531+ gnomekeyring.create_sync(name)
2532+
2533+ def _item_exists(self, sync, name):
2534+ """
2535+ Returns whether a named item exists in a
2536+ named keyring
2537+ """
2538+ return self._get_item_id_from_name(sync, name) != None
2539+
2540+ def _get_item_id_from_name(self, sync, name):
2541+ """
2542+ Returns the ID for a named item
2543+ """
2544+ for item_id in gnomekeyring.list_item_ids_sync(sync):
2545+ item_info = gnomekeyring.item_get_info_sync(sync, item_id)
2546+ display_name = item_info.get_display_name()
2547+ if display_name == name:
2548+ return item_id
2549+ return None
2550+
2551+ def _get_item_info_from_name(self, sync, name):
2552+ """
2553+ Returns the ID for a named item
2554+ """
2555+ for item_id in gnomekeyring.list_item_ids_sync(sync):
2556+ item_info = gnomekeyring.item_get_info_sync(sync, item_id)
2557+ display_name = item_info.get_display_name()
2558+ if display_name == name:
2559+ return item_info
2560+ return None
2561+
2562+ def set_ubuntusso_attr(self, attr):
2563+ """
2564+ Sets the attributes of the Ubuntu SSO item
2565+ """
2566+ # Creates the secret from the attributes
2567+ secret = urlencode(attr)
2568+
2569+ # Create the keyring
2570+ self._create_keyring(self.KEYRING_NAME)
2571+
2572+ # If the item already exists, delete it
2573+ if self._item_exists(self.KEYRING_NAME, self.app_name):
2574+ item_id = self._get_item_id_from_name(self.KEYRING_NAME,
2575+ self.app_name)
2576+ gnomekeyring.item_delete_sync(self.KEYRING_NAME, item_id)
2577+
2578+ # Add our SSO item
2579+ gnomekeyring.item_create_sync(self.KEYRING_NAME,
2580+ gnomekeyring.ITEM_GENERIC_SECRET, self.app_name, {},
2581+ secret, True)
2582+
2583+ def get_ubuntusso_attr(self):
2584+ """
2585+ Returns the secret of the SSO item in a dictionary
2586+ """
2587+ # If we have no attributes, return None
2588+ if self._get_item_info_from_name(self.KEYRING_NAME,
2589+ self.app_name) == None:
2590+ return None
2591+ secret = self._get_item_info_from_name(self.KEYRING_NAME,
2592+ self.app_name).get_secret()
2593+ return dict(parse_qsl(secret))
2594+
2595+if __name__ == "__main__":
2596+ SSO_ITEM_NAME = "Software Center UbuntuSSO token"
2597+ keyring = Keyring(SSO_ITEM_NAME)
2598+ keyring.set_ubuntusso_attr({"ha":"hehddddeff", "hi":"hggehes", "ho":"he"})
2599+ print keyring.get_ubuntusso_attr()
2600+
2601+
2602
2603=== modified file 'ubuntu_sso/logger.py'
2604--- ubuntu_sso/logger.py 2010-06-16 15:11:04 +0000
2605+++ ubuntu_sso/logger.py 2010-08-11 18:19:01 +0000
2606@@ -15,9 +15,13 @@
2607 #
2608 # You should have received a copy of the GNU General Public License along
2609 # with this program. If not, see <http://www.gnu.org/licenses/>.
2610+
2611 """Miscellaneous logging functions."""
2612+
2613+import logging
2614 import os
2615-import logging
2616+import sys
2617+
2618 import xdg.BaseDirectory
2619
2620 from logging.handlers import RotatingFileHandler
2621@@ -31,7 +35,7 @@
2622 LOGFILENAME = os.path.join(LOGFOLDER, 'oauth-login.log')
2623
2624 # Only log this level and above
2625-LOG_LEVEL = logging.INFO
2626+LOG_LEVEL = logging.DEBUG # logging.INFO
2627
2628 root_formatter = logging.Formatter(
2629 fmt="%(asctime)s:%(msecs)s %(name)s %(message)s")
2630@@ -40,10 +44,16 @@
2631 root_handler.setLevel(LOG_LEVEL)
2632 root_handler.setFormatter(root_formatter)
2633
2634+
2635 def setupLogging(log_domain):
2636 """Create basic logger to set filename"""
2637 logger = logging.getLogger(log_domain)
2638 logger.propagate = False
2639 logger.setLevel(LOG_LEVEL)
2640 logger.addHandler(root_handler)
2641+ if os.environ.get('DEBUG'):
2642+ debug_handler = logging.StreamHandler(sys.stderr)
2643+ debug_handler.setLevel(logging.DEBUG)
2644+ logger.addHandler(debug_handler)
2645+
2646 return logger
2647
2648=== modified file 'ubuntu_sso/main.py'
2649--- ubuntu_sso/main.py 2010-06-16 15:11:04 +0000
2650+++ ubuntu_sso/main.py 2010-08-11 18:19:01 +0000
2651@@ -3,6 +3,8 @@
2652 # ubuntu_sso.main - main login handling interface
2653 #
2654 # Author: Stuart Langridge <stuart.langridge@canonical.com>
2655+# Author: Natalia Bidart <natalia.bidart@canonical.com>
2656+# Author: Alejandro J. Cura <alecu@canonical.com>
2657 #
2658 # Copyright 2009 Canonical Ltd.
2659 #
2660@@ -25,39 +27,485 @@
2661 signal so they can retrieve the new token.
2662 """
2663
2664-import dbus.service, urlparse, time, gobject
2665-import pynotify
2666+import re
2667+import socket
2668+import time
2669+import threading
2670+import urllib
2671+import urllib2
2672+import urlparse
2673+
2674+import dbus.service
2675+import gobject
2676
2677 from dbus.mainloop.glib import DBusGMainLoop
2678+from lazr.restfulclient.authorize import BasicHttpAuthorizer
2679+from lazr.restfulclient.authorize.oauth import OAuthAuthorizer
2680+from lazr.restfulclient.errors import HTTPError
2681+from lazr.restfulclient.resource import ServiceRoot
2682+from oauth.oauth import OAuthToken
2683
2684-from ubuntu_sso import DBUS_IFACE_AUTH_NAME
2685+from keyring import Keyring
2686+from ubuntu_sso import (DBUS_IFACE_AUTH_NAME, DBUS_IFACE_USER_NAME,
2687+ DBUS_IFACE_CRED_NAME, DBUS_CRED_PATH, DBUS_BUS_NAME, gui)
2688 from ubuntu_sso.config import get_config
2689-
2690 from ubuntu_sso.logger import setupLogging
2691 logger = setupLogging("ubuntu_sso.main")
2692
2693 DBusGMainLoop(set_as_default=True)
2694-
2695 # Disable the invalid name warning, as we have a lot of DBus style names
2696 # pylint: disable-msg=C0103
2697
2698+
2699 class NoDefaultConfigError(Exception):
2700 """No default section in configuration file"""
2701- pass
2702+
2703
2704 class BadRealmError(Exception):
2705- """Realm must be a URL"""
2706- pass
2707+ """Realm must be a URL."""
2708+
2709+
2710+class InvalidEmailError(Exception):
2711+ """The email is not valid."""
2712+
2713+
2714+class InvalidPasswordError(Exception):
2715+ """The password is not valid.
2716+
2717+ Must provide at least 8 characters, one upper case, one number.
2718+ """
2719+
2720+
2721+class RegistrationError(Exception):
2722+ """The registration failed."""
2723+
2724+
2725+class AuthenticationError(Exception):
2726+ """The authentication failed."""
2727+
2728+
2729+class EmailTokenError(Exception):
2730+ """The email token is not valid."""
2731+
2732+
2733+class ResetPasswordTokenError(Exception):
2734+ """The token for password reset could not be generated."""
2735+
2736+
2737+class NewPasswordError(Exception):
2738+ """The new password could not be set."""
2739+
2740+
2741+def keyring_store_credentials(app_name, credentials):
2742+ """Store the credentials in the keyring."""
2743+ Keyring(app_name).set_ubuntusso_attr(credentials)
2744+
2745+
2746+def keyring_get_credentials(app_name):
2747+ """Get the credentials from the keyring or None if not there."""
2748+ return Keyring(app_name).get_ubuntusso_attr()
2749+
2750+
2751+def get_token_name(app_name):
2752+ """Build the token name."""
2753+ quoted_app_name = urllib.quote(app_name)
2754+ computer_name = socket.gethostname()
2755+ quoted_computer_name = urllib.quote(computer_name)
2756+ return "%s - %s" % (quoted_app_name, quoted_computer_name)
2757+
2758+
2759+class SSOLoginProcessor(object):
2760+ """Login and register users using the Ubuntu Single Sign On service."""
2761+
2762+ def __init__(self, sso_service_class=None):
2763+ """Create a new SSO login processor."""
2764+ if sso_service_class is None:
2765+ self.sso_service_class = ServiceRoot
2766+ else:
2767+ self.sso_service_class = sso_service_class
2768+
2769+ self.service_url = 'https://login.ubuntu.com/api/1.0'
2770+
2771+ def _valid_email(self, email):
2772+ """Validate the given email."""
2773+ # XXX: to be improved as per django validation
2774+ return email is not None and '@' in email
2775+
2776+ def _valid_password(self, password):
2777+ """Validate the given password."""
2778+ res = (len(password) > 7 and # at least 8 characters
2779+ re.search('[A-Z]', password) and # one upper case
2780+ re.search('\d+', password)) # one number
2781+ return res
2782+
2783+ def generate_captcha(self, filename):
2784+ """Generate a captcha using the SSO service."""
2785+ logger.debug('generate_captcha: requesting captcha, filename: %r',
2786+ filename)
2787+ sso_service = self.sso_service_class(None, self.service_url)
2788+ captcha = sso_service.captchas.new()
2789+
2790+ # download captcha and save to 'filename'
2791+ logger.debug('generate_captcha: server answered: %r', captcha)
2792+ try:
2793+ res = urllib2.urlopen(captcha['image_url'])
2794+ with open(filename, 'wb') as f:
2795+ f.write(res.read())
2796+ except:
2797+ msg = 'generate_captcha crashed while downloading the image.'
2798+ logger.exception(msg)
2799+ raise
2800+
2801+ return captcha['captcha_id']
2802+
2803+ def register_user(self, email, password, captcha_id, captcha_solution):
2804+ """Register a new user with 'email' and 'password'."""
2805+ logger.debug('register_user: email: %r password: <hidden>, '
2806+ 'captcha_id: %r, captcha_solution: %r',
2807+ email, captcha_id, captcha_solution)
2808+ sso_service = self.sso_service_class(None, self.service_url)
2809+ if not self._valid_email(email):
2810+ logger.error('register_user: InvalidEmailError for email: %r',
2811+ email)
2812+ raise InvalidEmailError()
2813+ if not self._valid_password(password):
2814+ logger.error('register_user: InvalidPasswordError')
2815+ raise InvalidPasswordError()
2816+
2817+ result = sso_service.registrations.register(
2818+ email=email, password=password, captcha_id=captcha_id,
2819+ captcha_solution=captcha_solution)
2820+ logger.info('register_user: email: %r result: %r', email, result)
2821+
2822+ if result['status'] == 'error':
2823+ raise RegistrationError(result['errors'])
2824+ elif result['status'] != 'ok':
2825+ raise RegistrationError('Received unknown status: %s' % result)
2826+ else:
2827+ return True
2828+
2829+ def login(self, email, password, app_name):
2830+ """Login a user with 'email' and 'password'."""
2831+ logger.debug('login: email: %r password: <hidden>, app_name: %r',
2832+ email, app_name)
2833+ basic = BasicHttpAuthorizer(email, password)
2834+ sso_service = self.sso_service_class(basic, self.service_url)
2835+ service = sso_service.authentications.authenticate
2836+
2837+ token_name = get_token_name(app_name)
2838+ try:
2839+ credentials = service(token_name=token_name)
2840+ except HTTPError:
2841+ logger.exception('login failed with:')
2842+ raise AuthenticationError()
2843+
2844+ logger.debug('login: authentication successful! consumer_key: %r',
2845+ credentials['consumer_key'])
2846+ keyring_store_credentials(app_name, credentials)
2847+ return credentials
2848+
2849+ def validate_email(self, email, password, app_name, email_token):
2850+ """Validate an email token for user with 'email' and 'password'."""
2851+ logger.debug('validate_email: email: %r password: <hidden>, '
2852+ 'app_name: %r, email_token: %r',
2853+ email, app_name, email_token)
2854+ token = self.login(email=email, password=password,
2855+ app_name=app_name)
2856+
2857+ oauth_token = OAuthToken(token['token'], token['token_secret'])
2858+ authorizer = OAuthAuthorizer(token['consumer_key'],
2859+ token['consumer_secret'],
2860+ oauth_token)
2861+ sso_service = self.sso_service_class(authorizer, self.service_url)
2862+ result = sso_service.accounts.validate_email(email_token=email_token)
2863+ logger.info('validate_email: email: %r result: %r', email, result)
2864+ if 'errors' in result:
2865+ raise EmailTokenError(result['errors'])
2866+ elif 'email' in result:
2867+ return True
2868+ else:
2869+ raise EmailTokenError('Received invalid reply: %s' % result)
2870+
2871+ def request_password_reset_token(self, email):
2872+ """Request a token to reset the password for the account 'email'."""
2873+ sso_service = self.sso_service_class(None, self.service_url)
2874+ service = sso_service.registrations.request_password_reset_token
2875+ try:
2876+ result = service(email=email)
2877+ except HTTPError, e:
2878+ logger.exception('request_password_reset_token failed with:')
2879+ raise ResetPasswordTokenError(e.content)
2880+
2881+ if result['status'] == 'ok':
2882+ return True
2883+ else:
2884+ raise ResetPasswordTokenError('Received invalid reply: %s' %
2885+ result)
2886+
2887+ def set_new_password(self, email, token, new_password):
2888+ """Set a new password for the account 'email' to be 'new_password'.
2889+
2890+ The 'token' has to be the one resulting from a call to
2891+ 'request_password_reset_token'.
2892+
2893+ """
2894+ sso_service = self.sso_service_class(None, self.service_url)
2895+ service = sso_service.registrations.set_new_password
2896+ try:
2897+ result = service(email=email, token=token,
2898+ new_password=new_password)
2899+ except HTTPError, e:
2900+ logger.exception('set_new_password failed with:')
2901+ raise NewPasswordError(e.content)
2902+
2903+ if result['status'] == 'ok':
2904+ return True
2905+ else:
2906+ raise NewPasswordError('Received invalid reply: %s' % result)
2907+
2908+
2909+def blocking(f, result_cb, error_cb):
2910+ """Run f in a thread; return or throw an exception thru the callbacks."""
2911+ def _in_thread():
2912+ """The part that runs inside the thread."""
2913+ try:
2914+ result_cb(f())
2915+ except Exception, e:
2916+ msg = "Exception while running DBus blocking code in a thread."""
2917+ logger.exception(msg)
2918+ error_cb(str(e))
2919+ threading.Thread(target=_in_thread).start()
2920+
2921+
2922+class SSOLogin(dbus.service.Object):
2923+ """Login thru the Single Sign On service."""
2924+
2925+ def __init__(self, bus_name,
2926+ sso_login_processor_class=SSOLoginProcessor,
2927+ sso_service_class=None):
2928+ """Initiate the Login object."""
2929+ dbus.service.Object.__init__(self, object_path="/sso",
2930+ bus_name=bus_name)
2931+ self.sso_login_processor_class = sso_login_processor_class
2932+ self.sso_service_class = sso_service_class
2933+
2934+ def processor(self):
2935+ """Create a login processor with the given class and service class."""
2936+ return self.sso_login_processor_class(
2937+ sso_service_class=self.sso_service_class)
2938+
2939+ # generate_capcha signals
2940+ @dbus.service.signal(DBUS_IFACE_USER_NAME, signature="s")
2941+ def CaptchaGenerated(self, result):
2942+ """Signal thrown after the captcha is generated."""
2943+
2944+ @dbus.service.signal(DBUS_IFACE_USER_NAME, signature="s")
2945+ def CaptchaGenerationError(self, error):
2946+ """Signal thrown when there's a problem generating the captcha."""
2947+
2948+ @dbus.service.method(dbus_interface=DBUS_IFACE_USER_NAME,
2949+ in_signature='s')
2950+ def generate_captcha(self, filename):
2951+ """Call the matching method in the processor."""
2952+ def f():
2953+ """Inner function that will be run in a thread."""
2954+ return self.processor().generate_captcha(filename)
2955+ blocking(f, self.CaptchaGenerated, self.CaptchaGenerationError)
2956+
2957+ # register_user signals
2958+ @dbus.service.signal(DBUS_IFACE_USER_NAME, signature="b")
2959+ def UserRegistered(self, result):
2960+ """Signal thrown when the user is registered."""
2961+
2962+ @dbus.service.signal(DBUS_IFACE_USER_NAME, signature="s")
2963+ def UserRegistrationError(self, error):
2964+ """Signal thrown when there's a problem registering the user."""
2965+
2966+ @dbus.service.method(dbus_interface=DBUS_IFACE_USER_NAME,
2967+ in_signature='ssss')
2968+ def register_user(self, email, password, captcha_id, captcha_solution):
2969+ """Call the matching method in the processor."""
2970+ def f():
2971+ """Inner function that will be run in a thread."""
2972+ return self.processor().register_user(email, password,
2973+ captcha_id, captcha_solution)
2974+ blocking(f, self.UserRegistered, self.UserRegistrationError)
2975+
2976+ # login signals
2977+ @dbus.service.signal(DBUS_IFACE_USER_NAME, signature="s")
2978+ def LoggedIn(self, result):
2979+ """Signal thrown when the user is logged in."""
2980+
2981+ @dbus.service.signal(DBUS_IFACE_USER_NAME, signature="s")
2982+ def LoginError(self, error):
2983+ """Signal thrown when there is a problem in the login."""
2984+
2985+ @dbus.service.method(dbus_interface=DBUS_IFACE_USER_NAME,
2986+ in_signature='sss')
2987+ def login(self, email, password, app_name):
2988+ """Call the matching method in the processor."""
2989+ def f():
2990+ """Inner function that will be run in a thread."""
2991+ credentials = self.processor().login(email, password, app_name)
2992+ assert credentials is not None
2993+ return app_name
2994+ blocking(f, self.LoggedIn, self.LoginError)
2995+
2996+ # validate_email signals
2997+ @dbus.service.signal(DBUS_IFACE_USER_NAME, signature="s")
2998+ def EmailValidated(self, app_name):
2999+ """Signal thrown after the email is validated."""
3000+
3001+ @dbus.service.signal(DBUS_IFACE_USER_NAME, signature="s")
3002+ def EmailValidationError(self, error):
3003+ """Signal thrown when there's a problem validating the email."""
3004+
3005+ @dbus.service.method(dbus_interface=DBUS_IFACE_USER_NAME,
3006+ in_signature='ssss')
3007+ def validate_email(self, email, password, app_name, email_token):
3008+ """Call the matching method in the processor."""
3009+ def f():
3010+ """Inner function that will be run in a thread."""
3011+ self.processor().validate_email(email, password,
3012+ app_name, email_token)
3013+ return app_name
3014+ blocking(f, self.EmailValidated, self.EmailValidationError)
3015+
3016+ # request_password_reset_token signals
3017+ @dbus.service.signal(DBUS_IFACE_USER_NAME, signature="s")
3018+ def PasswordResetTokenSent(self, email):
3019+ """Signal thrown when the token is succesfully sent."""
3020+
3021+ @dbus.service.signal(DBUS_IFACE_USER_NAME, signature="s")
3022+ def PasswordResetError(self, error):
3023+ """Signal thrown when there's a problem sending the token."""
3024+
3025+ @dbus.service.method(dbus_interface=DBUS_IFACE_USER_NAME,
3026+ in_signature='s')
3027+ def request_password_reset_token(self, email):
3028+ """Call the matching method in the processor."""
3029+ def f():
3030+ """Inner function that will be run in a thread."""
3031+ self.processor().request_password_reset_token(email)
3032+ return email
3033+ blocking(f, self.PasswordResetTokenSent, self.PasswordResetError)
3034+
3035+ # set_new_password signals
3036+ @dbus.service.signal(DBUS_IFACE_USER_NAME, signature="s")
3037+ def PasswordChanged(self, email):
3038+ """Signal thrown when the token is succesfully sent."""
3039+
3040+ @dbus.service.signal(DBUS_IFACE_USER_NAME, signature="s")
3041+ def PasswordChangeError(self, error):
3042+ """Signal thrown when there's a problem sending the token."""
3043+
3044+ @dbus.service.method(dbus_interface=DBUS_IFACE_USER_NAME,
3045+ in_signature='sss')
3046+ def set_new_password(self, email, token, new_password):
3047+ """Call the matching method in the processor."""
3048+ def f():
3049+ """Inner function that will be run in a thread."""
3050+ self.processor().set_new_password(email, token, new_password)
3051+ return email
3052+ blocking(f, self.PasswordChanged, self.PasswordChangeError)
3053+
3054+
3055+class SSOCredentials(dbus.service.Object):
3056+ """DBus object that gets credentials, and login/registers if needed."""
3057+
3058+ @dbus.service.signal(DBUS_IFACE_CRED_NAME, signature="s")
3059+ def AuthorizationDenied(self, app_name):
3060+ """Signal thrown when the user denies the authorization."""
3061+
3062+ @dbus.service.signal(DBUS_IFACE_CRED_NAME, signature="a{ss}")
3063+ def CredentialsFound(self, app_name):
3064+ """Signal thrown when the credentials are found."""
3065+
3066+ @dbus.service.signal(DBUS_IFACE_CRED_NAME, signature="s")
3067+ def CredentialsError(self, error):
3068+ """Signal thrown when there is a problem finding the credentials."""
3069+
3070+ @dbus.service.method(dbus_interface=DBUS_IFACE_CRED_NAME,
3071+ in_signature="s", out_signature="a{ss}")
3072+ def find_credentials(self, app_name):
3073+ """Get the credentials from the keyring or '' if not there."""
3074+ token = keyring_get_credentials(app_name)
3075+ if token is None:
3076+ return {}
3077+ else:
3078+ return token
3079+
3080+ def _login_success_cb(self, dialog, app_name):
3081+ """Handles the response from the UI dialog."""
3082+ try:
3083+ creds = keyring_get_credentials(app_name)
3084+ self.CredentialsFound(creds)
3085+ except Exception, e:
3086+ logger.exception("problem when getting credentials from keyring")
3087+ self.CredentialsError(str(e))
3088+
3089+ def _login_error_cb(self, dialog):
3090+ """Handles a problem in the UI."""
3091+ msg = "misc problem getting the credentials"
3092+ self.CredentialsError(msg)
3093+ logger.error(msg)
3094+
3095+ def _login_auth_denied_cb(self, dialog):
3096+ """The user decides not to allow the registration or login."""
3097+ self.AuthorizationDenied()
3098+
3099+ def _show_login_or_register_ui(self, app_name, tc_url, help_text, win_id):
3100+ """Shows the UI so the user can login or register."""
3101+ try:
3102+ gui_app = gui.UbuntuSSOClientGUI(app_name, tc_url,
3103+ help_text, win_id)
3104+ gui_app.connect(gui.SIG_LOGIN_SUCCEEDED, self._login_success_cb)
3105+ gui_app.connect(gui.SIG_LOGIN_FAILED, self._login_error_cb)
3106+ gui_app.connect(gui.SIG_REGISTRATION_SUCCEEDED,
3107+ self._login_success_cb)
3108+ gui_app.connect(gui.SIG_REGISTRATION_FAILED, self._login_error_cb)
3109+ gui_app.connect(gui.SIG_USER_CANCELATION,
3110+ self._login_auth_denied_cb)
3111+ except Exception, e:
3112+ msg = '_show_login_or_register_ui failed when calling ' \
3113+ 'gui.UbuntuSSOClientGUI(%r, %r, %r, %r)'
3114+ logger.exception(msg, app_name, tc_url, help_text, win_id)
3115+ self.CredentialsError(str(e))
3116+
3117+ @dbus.service.method(dbus_interface=DBUS_IFACE_CRED_NAME,
3118+ in_signature="sssx", out_signature="")
3119+ def login_or_register_to_get_credentials(self, app_name,
3120+ terms_and_conditions_url,
3121+ help_text, window_id):
3122+ """Get credentials if found else prompt GUI to login or register.
3123+
3124+ 'app_name' will be displayed in the GUI.
3125+ 'terms_and_conditions_url' will be the URL pointing to T&C.
3126+ 'help_text' is an explanatory text for the end-users, will be shown
3127+ below the headers.
3128+ 'window_id' is the id of the window which will be set as a parent of
3129+ the GUI. If 0, no parent will be set.
3130+
3131+ """
3132+ try:
3133+ token = keyring_get_credentials(app_name)
3134+ if token is None:
3135+ gobject.idle_add(self._show_login_or_register_ui,
3136+ app_name, terms_and_conditions_url,
3137+ help_text, window_id)
3138+ else:
3139+ self.CredentialsFound(token)
3140+ except Exception, e:
3141+ logger.exception("problem while getting the credentials")
3142+ self.CredentialsError(str(e))
3143+
3144
3145 class LoginProcessor:
3146- """Actually do the work of processing passed parameters"""
3147- def __init__(self, dbus_object, use_libnotify=True):
3148+ """Actually do the work of processing passed parameters."""
3149+
3150+ def __init__(self, dbus_object):
3151 """Initialize the login processor."""
3152 logger.debug("Creating a LoginProcessor")
3153- self.use_libnotify = use_libnotify
3154- if self.use_libnotify and pynotify:
3155- logger.debug("Hooking libnotify")
3156- pynotify.init("UbuntuOne Login")
3157 self.note1 = None
3158 self.realm = None
3159 self.consumer_key = None
3160@@ -68,13 +516,13 @@
3161 def login(self, realm, consumer_key, do_login=True):
3162 """Initiate an OAuth login"""
3163 logger.debug("Initiating OAuth login in LoginProcessor")
3164- self.realm = str(realm) # because they are dbus.Strings, not str
3165+ self.realm = str(realm) # because they are dbus.Strings, not str
3166 self.consumer_key = str(consumer_key)
3167
3168 logger.debug("Obtaining OAuth urls")
3169 (request_token_url, user_authorisation_url,
3170- access_token_url, consumer_secret) = self.get_config_urls(realm)
3171- logger.debug("OAuth URLs are: request='%s', userauth='%s', " +\
3172+ access_token_url, consumer_secret) = self.get_config_urls(realm)
3173+ logger.debug("OAuth URLs are: request='%s', userauth='%s', " + \
3174 "access='%s', secret='%s'", request_token_url,
3175 user_authorisation_url, access_token_url, consumer_secret)
3176
3177@@ -98,7 +546,7 @@
3178 self.realm = str(realm)
3179 self.consumer_key = str(consumer_key)
3180 (request_token_url, user_authorisation_url,
3181- access_token_url, consumer_secret) = self.get_config_urls(self.realm)
3182+ access_token_url, consumer_secret) = self.get_config_urls(self.realm)
3183 from ubuntu_sso.auth import AuthorisationClient
3184 client = AuthorisationClient(self.realm,
3185 request_token_url,
3186@@ -114,7 +562,7 @@
3187 def error_handler(self, failure):
3188 """Deal with errors returned from auth process"""
3189 logger.debug("Error returned from auth process")
3190- self.dbus_object.currently_authing = False # not block future requests
3191+ self.dbus_object.currently_authing = False # not block future requests
3192
3193 def get_config_urls(self, realm):
3194 """Look up the URLs to use in the config file"""
3195@@ -209,7 +657,8 @@
3196
3197
3198 class Login(dbus.service.Object):
3199- """Object which listens for D-Bus OAuth requests"""
3200+ """Object which listens for D-Bus OAuth requests."""
3201+
3202 def __init__(self, bus_name):
3203 """Initiate the Login object."""
3204 dbus.service.Object.__init__(self, object_path="/", bus_name=bus_name)
3205@@ -218,9 +667,9 @@
3206 logger.debug("Login D-Bus service starting up")
3207
3208 @dbus.service.method(dbus_interface=DBUS_IFACE_AUTH_NAME,
3209- in_signature='ss', out_signature='')
3210+ in_signature='ss', out_signature='')
3211 def login(self, realm, consumer_key):
3212- """D-Bus method, exported over the bus, to initiate an OAuth login"""
3213+ """D-Bus method, to initiate an OAuth login."""
3214 logger.debug("login() D-Bus message received with realm='%s', " +
3215 "consumer_key='%s'", realm, consumer_key)
3216 if self.currently_authing:
3217@@ -230,11 +679,9 @@
3218 self.processor.login(realm, consumer_key)
3219
3220 @dbus.service.method(dbus_interface=DBUS_IFACE_AUTH_NAME,
3221- in_signature='ssb', out_signature='')
3222+ in_signature='ssb', out_signature='')
3223 def maybe_login(self, realm, consumer_key, do_login):
3224- """
3225- D-Bus method, exported over the bus, to maybe initiate an OAuth login
3226- """
3227+ """D-Bus method, to maybe initiate an OAuth login."""
3228 logger.debug("maybe_login() D-Bus message received with realm='%s', " +
3229 "consumer_key='%s'", realm, consumer_key)
3230 if self.currently_authing:
3231@@ -246,9 +693,7 @@
3232 @dbus.service.method(dbus_interface=DBUS_IFACE_AUTH_NAME,
3233 in_signature='ss', out_signature='')
3234 def clear_token(self, realm, consumer_key):
3235- """
3236- D-Bus method, exported over the bus, to clear the existing token.
3237- """
3238+ """D-Bus method, to clear the existing token."""
3239 self.processor.clear_token(realm, consumer_key)
3240
3241 @dbus.service.signal(dbus_interface=DBUS_IFACE_AUTH_NAME, signature='ss')
3242@@ -274,17 +719,25 @@
3243 self.currently_authing = False
3244 return message
3245
3246+
3247 def main():
3248 """Start everything"""
3249+ dbus.mainloop.glib.threads_init()
3250+ gobject.threads_init()
3251 logger.debug("Starting up at %s", time.asctime())
3252 logger.debug("Installing the Twisted glib2reactor")
3253- from twisted.internet import glib2reactor # for non-GUI apps
3254+ from twisted.internet import glib2reactor # for non-GUI apps
3255 glib2reactor.install()
3256 from twisted.internet import reactor
3257
3258 logger.debug("Creating the D-Bus service")
3259- Login(dbus.service.BusName(DBUS_IFACE_AUTH_NAME,
3260- bus=dbus.SessionBus()))
3261+ Login(dbus.service.BusName(DBUS_BUS_NAME,
3262+ bus=dbus.SessionBus()))
3263+ SSOLogin(dbus.service.BusName(DBUS_BUS_NAME,
3264+ bus=dbus.SessionBus()))
3265+ SSOCredentials(dbus.service.BusName(DBUS_BUS_NAME,
3266+ bus=dbus.SessionBus()),
3267+ object_path=DBUS_CRED_PATH)
3268 # cleverness here to say:
3269 # am I already running (bound to this d-bus name)?
3270 # if so, send a signal to the already running instance
3271@@ -292,4 +745,3 @@
3272 # to kick off the signin process
3273 logger.debug("Starting the reactor mainloop")
3274 reactor.run()
3275-
3276
3277=== added file 'ubuntu_sso/networkstate.py'
3278--- ubuntu_sso/networkstate.py 1970-01-01 00:00:00 +0000
3279+++ ubuntu_sso/networkstate.py 2010-08-11 18:19:01 +0000
3280@@ -0,0 +1,106 @@
3281+# networkstate - detect the current state of the network
3282+#
3283+# Author: Alejandro J. Cura <alecu@canonical.com>
3284+#
3285+# Copyright 2010 Canonical Ltd.
3286+#
3287+# This program is free software: you can redistribute it and/or modify it
3288+# under the terms of the GNU General Public License version 3, as published
3289+# by the Free Software Foundation.
3290+#
3291+# This program is distributed in the hope that it will be useful, but
3292+# WITHOUT ANY WARRANTY; without even the implied warranties of
3293+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
3294+# PURPOSE. See the GNU General Public License for more details.
3295+#
3296+# You should have received a copy of the GNU General Public License along
3297+# with this program. If not, see <http://www.gnu.org/licenses/>.
3298+"""Implementation of network state detection."""
3299+
3300+import dbus
3301+
3302+from ubuntu_sso.logger import setupLogging
3303+logger = setupLogging("ubuntu_sso.networkstate")
3304+
3305+# Values returned by the callback
3306+ONLINE, OFFLINE, UNKNOWN = object(), object(), object()
3307+
3308+nm_state_names = {
3309+ ONLINE: "online",
3310+ OFFLINE: "offline",
3311+ UNKNOWN: "unknown",
3312+}
3313+
3314+# Internal NetworkManager State constants
3315+NM_STATE_UNKNOWN = 0
3316+NM_STATE_ASLEEP = 1
3317+NM_STATE_CONNECTING = 2
3318+NM_STATE_CONNECTED = 3
3319+NM_STATE_DISCONNECTED = 4
3320+
3321+NM_DBUS_INTERFACE = "org.freedesktop.NetworkManager"
3322+NM_DBUS_OBJECTPATH = "/org/freedesktop/NetworkManager"
3323+DBUS_UNKNOWN_SERVICE = "org.freedesktop.DBus.Error.ServiceUnknown"
3324+
3325+
3326+class NetworkManagerState(object):
3327+ """Checks the state of NetworkManager thru DBus."""
3328+
3329+ def __init__(self, result_cb, dbus=dbus):
3330+ """Initialize this instance with a result and error callbacks."""
3331+ self.result_cb = result_cb
3332+ self.dbus = dbus
3333+ self.state_signal = None
3334+
3335+ def call_result_cb(self, state):
3336+ """Return the state thru the result callback."""
3337+ if self.state_signal:
3338+ self.state_signal.remove()
3339+ self.result_cb(state)
3340+
3341+ def got_state(self, state):
3342+ """Called by DBus when the state is retrieved from NM."""
3343+ if state == NM_STATE_CONNECTED:
3344+ self.call_result_cb(ONLINE)
3345+ elif state == NM_STATE_CONNECTING:
3346+ logger.debug("Currently connecting, waiting for signal")
3347+ else:
3348+ self.call_result_cb(OFFLINE)
3349+
3350+ def got_error(self, error):
3351+ """Called by DBus when the state is retrieved from NM."""
3352+ if isinstance(error, self.dbus.exceptions.DBusException) and \
3353+ error.get_dbus_name() == DBUS_UNKNOWN_SERVICE:
3354+ logger.debug("Network Manager not present")
3355+ self.call_result_cb(UNKNOWN)
3356+ else:
3357+ logger.error("Error contacting NetworkManager: %s" % \
3358+ str(error))
3359+ self.call_result_cb(UNKNOWN)
3360+
3361+ def state_changed(self, state):
3362+ """Called when a signal is emmited by Network Manager."""
3363+ if int(state) == NM_STATE_CONNECTED:
3364+ self.call_result_cb(ONLINE)
3365+ elif int(state) == NM_STATE_DISCONNECTED:
3366+ self.call_result_cb(OFFLINE)
3367+ else:
3368+ logger.debug("Not yet connected: continuing to wait")
3369+
3370+ def find_online_state(self):
3371+ """Get the network state and return it thru the set callback."""
3372+ try:
3373+ sysbus = self.dbus.SystemBus()
3374+ nm_proxy = sysbus.get_object(NM_DBUS_INTERFACE,
3375+ NM_DBUS_OBJECTPATH,
3376+ follow_name_owner_changes=True)
3377+ nm_if = self.dbus.Interface(nm_proxy, NM_DBUS_INTERFACE)
3378+ self.state_signal = nm_if.connect_to_signal(
3379+ signal_name="StateChanged",
3380+ handler_function=self.state_changed,
3381+ dbus_interface=NM_DBUS_INTERFACE)
3382+ nm_proxy.Get(NM_DBUS_INTERFACE, "State",
3383+ reply_handler=self.got_state,
3384+ error_handler=self.got_error)
3385+ except Exception, e:
3386+ self.got_error(e)
3387
3388=== added file 'ubuntu_sso/tests/test_gui.py'
3389--- ubuntu_sso/tests/test_gui.py 1970-01-01 00:00:00 +0000
3390+++ ubuntu_sso/tests/test_gui.py 2010-08-11 18:19:01 +0000
3391@@ -0,0 +1,1220 @@
3392+# -*- coding: utf-8 -*-
3393+#
3394+# test_gui - tests for ubuntu_sso.gui
3395+#
3396+# Author: Natalia Bidart <natalia.bidart@canonical.com>
3397+#
3398+# Copyright 2010 Canonical Ltd.
3399+#
3400+# This program is free software: you can redistribute it and/or modify it
3401+# under the terms of the GNU General Public License version 3, as published
3402+# by the Free Software Foundation.
3403+#
3404+# This program is distributed in the hope that it will be useful, but
3405+# WITHOUT ANY WARRANTY; without even the implied warranties of
3406+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
3407+# PURPOSE. See the GNU General Public License for more details.
3408+#
3409+# You should have received a copy of the GNU General Public License along
3410+# with this program. If not, see <http://www.gnu.org/licenses/>.
3411+"""Tests for the GUI for registration/login."""
3412+
3413+import itertools
3414+import logging
3415+import os
3416+import socket
3417+
3418+import dbus
3419+import gtk
3420+
3421+from twisted.trial.unittest import TestCase
3422+
3423+from contrib.testing.testcase import MementoHandler
3424+from ubuntu_sso import gui
3425+
3426+
3427+APP_NAME = 'The Super testing app!'
3428+TC_URI = 'http://localhost'
3429+HELP_TEXT = """Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam sed
3430+lorem nibh. Suspendisse gravida nulla non nunc suscipit pulvinar tempus ut
3431+augue. Morbi consequat, ligula a elementum pretium, dolor nulla tempus metus,
3432+sed viverra nisi risus non velit."""
3433+
3434+NAME = 'Juanito Pérez'
3435+EMAIL = 'test@example.com'
3436+PASSWORD = 'h3lloWorld'
3437+CAPTCHA_ID = 'test'
3438+CAPTCHA_SOLUTION = 'william Byrd'
3439+TOKEN_NAME = 'my-testing-host'
3440+EMAIL_TOKEN = 'B2Pgtf'
3441+APP_NAME = "my-testing-app"
3442+
3443+
3444+class FakedSSOBackend(object):
3445+ """Fake a SSO Backend (acts as a dbus.Interface as well)."""
3446+
3447+ def __init__(self, *args, **kwargs):
3448+ self._args = args
3449+ self._kwargs = kwargs
3450+ self._called = {}
3451+ for i in ('generate_captcha', 'login', 'register_user',
3452+ 'validate_email'):
3453+ setattr(self, i, self._record_call(i))
3454+
3455+ def _record_call(self, func_name):
3456+ """Store values when calling 'func_name'."""
3457+
3458+ def inner(*args, **kwargs):
3459+ """Fake 'func_name'."""
3460+ self._called[func_name] = (args, kwargs)
3461+
3462+ return inner
3463+
3464+
3465+class FakedDbusObject(object):
3466+ """Fake a dbus."""
3467+
3468+ def __init__(self, *args, **kwargs):
3469+ """Init."""
3470+ self._args = args
3471+ self._kwargs = kwargs
3472+
3473+
3474+class FakedSessionBus(object):
3475+ """Fake the session bus."""
3476+
3477+ def __init__(self):
3478+ """Init."""
3479+ self.obj = None
3480+ self.callbacks = {}
3481+
3482+ def add_signal_receiver(self, method, signal_name, dbus_interface):
3483+ """Add a signal receiver."""
3484+ self.callbacks[(dbus_interface, signal_name)] = method
3485+
3486+ def remove_signal_receiver(self, match, signal_name, dbus_interface):
3487+ """Remove the signal receiver."""
3488+ assert (dbus_interface, signal_name) in self.callbacks
3489+ del self.callbacks[(dbus_interface, signal_name)]
3490+
3491+ def get_object(self, bus_name, object_path, introspect=True,
3492+ follow_name_owner_changes=False, **kwargs):
3493+ """Return a faked proxy for the given remote object."""
3494+ if bus_name == gui.DBUS_BUS_NAME and object_path == gui.DBUS_PATH:
3495+ assert self.obj is None
3496+ kwargs = dict(object_path=object_path,
3497+ bus_name=bus_name, follow_name_owner_changes=True)
3498+ self.obj = FakedDbusObject(**kwargs)
3499+ return self.obj
3500+
3501+
3502+class BasicTestCase(TestCase):
3503+ """Test case with a helper tracker."""
3504+
3505+ def setUp(self):
3506+ """Init."""
3507+ self._called = False # helper
3508+
3509+ self.memento = MementoHandler()
3510+ self.memento.setLevel(logging.DEBUG)
3511+ gui.logger.addHandler(self.memento)
3512+
3513+ def tearDown(self):
3514+ """Clean up."""
3515+ self._called = False
3516+
3517+ def _set_called(self, *args, **kwargs):
3518+ """Set _called to True."""
3519+ self._called = (args, kwargs)
3520+
3521+
3522+class LabeledEntryTestCase(BasicTestCase):
3523+ """Test suite for the labeled entry."""
3524+
3525+ def setUp(self):
3526+ """Init."""
3527+ super(LabeledEntryTestCase, self).setUp()
3528+ self.label = 'Test me please'
3529+ self.entry = gui.LabeledEntry(label=self.label)
3530+
3531+ # we need a window to be able to realize ourselves
3532+ window = gtk.Window()
3533+ window.add(self.entry)
3534+ window.show_all()
3535+
3536+ def tearDown(self):
3537+ """Clean up."""
3538+ self.entry = None
3539+ super(LabeledEntryTestCase, self).tearDown()
3540+
3541+ def grab_focus(self, focus_in=True):
3542+ """Grab focus on widget, if None use self.entry."""
3543+ direction = 'in' if focus_in else 'out'
3544+ self.entry.emit('focus-%s-event' % direction, None)
3545+
3546+ def assert_correct_label(self):
3547+ """Check that the entry has the correct label."""
3548+ # text content is correct
3549+ msg = 'Text content must be "%s" (got "%s" instead).'
3550+ expected = self.label
3551+ actual = super(gui.LabeledEntry, self.entry).get_text()
3552+ self.assertEqual(expected, actual, msg % (expected, actual))
3553+
3554+ # text color is correct
3555+ msg = 'Text color must be "%s" (got "%s" instead).'
3556+ expected = gui.HELP_TEXT_COLOR
3557+ actual = self.entry.style.text[gtk.STATE_NORMAL]
3558+ self.assertEqual(expected, actual, msg % (expected, actual))
3559+
3560+ def test_initial_text(self):
3561+ """Entry have the correct text at startup."""
3562+ self.assert_correct_label()
3563+
3564+ def test_width_chars(self):
3565+ """Entry have the correct width."""
3566+ self.assertEqual(self.entry.get_width_chars(), gui.DEFAULT_WIDTH)
3567+
3568+ def test_tooltip(self):
3569+ """Entry have the correct tooltip."""
3570+ msg = 'Tooltip must be "%s" (got "%s" instead).'
3571+ expected = self.label
3572+ actual = self.entry.get_tooltip_text()
3573+ # tooltip is correct
3574+ self.assertEqual(expected, actual, msg % (expected, actual))
3575+
3576+ def test_clear_entry_on_focus_in(self):
3577+ """Entry are cleared when focused."""
3578+ self.grab_focus()
3579+
3580+ msg = 'Entry must be cleared on focus in.'
3581+ self.assertEqual('', self.entry.get_text(), msg)
3582+
3583+ def test_text_defaults_to_theme_color_when_focus_in(self):
3584+ """Entry restore its text color when focused in."""
3585+ self.patch(self.entry, 'modify_text', self._set_called)
3586+
3587+ self.grab_focus()
3588+
3589+ self.assertEqual(((gtk.STATE_NORMAL, None), {}), self._called,
3590+ 'Entry text color must be restore on focus in.')
3591+
3592+ def test_refill_entry_on_focus_out_if_no_input(self):
3593+ """Entry is re-filled with label when focused out if no user input."""
3594+
3595+ self.grab_focus() # grab focus
3596+ self.grab_focus(focus_in=False) # loose focus
3597+
3598+ # Entry must be re-filled on focus out
3599+ self.assert_correct_label()
3600+
3601+ def test_refill_entry_on_focus_out_if_empty_input(self):
3602+ """Entry is re-filled with label when focused out if empty input."""
3603+
3604+ self.grab_focus() # grab focus
3605+
3606+ self.entry.set_text(' ') # add empty text to the entry
3607+
3608+ self.grab_focus(focus_in=False) # loose focus
3609+
3610+ # Entry must be re-filled on focus out
3611+ self.assert_correct_label()
3612+
3613+ def test_preserve_input_on_focus_out_if_user_input(self):
3614+ """Entry is unmodified when focused out if user input."""
3615+ msg = 'Entry must be left unmodified on focus out when user input.'
3616+ expected = 'test me please'
3617+
3618+ self.grab_focus() # grab focus
3619+
3620+ self.entry.set_text(expected) # add empty text to the entry
3621+
3622+ self.grab_focus(focus_in=False) # loose focus
3623+
3624+ self.assertEqual(expected, self.entry.get_text(), msg)
3625+
3626+ def test_preserve_input_on_focus_out_and_in_again(self):
3627+ """Entry is unmodified when focused out and then in again."""
3628+ msg = 'Entry must be left unmodified on focus out and then in again.'
3629+ expected = 'test me I mean it'
3630+
3631+ self.grab_focus() # grab focus
3632+
3633+ self.entry.set_text(expected) # add text to the entry
3634+
3635+ self.grab_focus(focus_in=False) # loose focus
3636+ self.grab_focus() # grab focus again!
3637+
3638+ self.assertEqual(expected, self.entry.get_text(), msg)
3639+
3640+ def test_get_text_ignores_label(self):
3641+ """Entry's text is only user input (label is ignored)."""
3642+ self.assertEqual(self.entry.get_text(), '')
3643+
3644+ def test_get_text_ignores_empty_input(self):
3645+ """Entry's text is only user input (empty text is ignored)."""
3646+ self.entry.set_text(' ')
3647+ self.assertEqual(self.entry.get_text(), '')
3648+
3649+ def test_get_text_doesnt_ignore_user_input(self):
3650+ """Entry's text is user input."""
3651+ self.entry.set_text('a')
3652+ self.assertEqual(self.entry.get_text(), 'a')
3653+
3654+
3655+class PasswordLabeledEntryTestCase(LabeledEntryTestCase):
3656+ """Test suite for the labeled entry when is_password is True."""
3657+
3658+ def setUp(self):
3659+ """Init."""
3660+ super(PasswordLabeledEntryTestCase, self).setUp()
3661+ self.entry.is_password = True
3662+
3663+ def test_password_fields_are_visible_at_startup(self):
3664+ """Password entrys show the helping text at startup."""
3665+ self.assertTrue(self.entry.get_visibility(),
3666+ 'Password entry should be visible at start up.')
3667+
3668+ def test_password_field_is_visible_if_no_input_and_focus_out(self):
3669+ """Password entry show the label when focus out."""
3670+ self.grab_focus() # user cliked or TAB'd to the entry
3671+ self.grab_focus(focus_in=False) # loose focus
3672+ self.assertTrue(self.entry.get_visibility(),
3673+ 'Entry should be visible when focus out and no input.')
3674+
3675+ def test_password_fields_are_not_visible_when_editing(self):
3676+ """Password entrys show the hidden chars instead of the password."""
3677+ self.grab_focus() # user cliked or TAB'd to the entry
3678+ self.assertFalse(self.entry.get_visibility(),
3679+ 'Entry should not be visible when editing.')
3680+
3681+
3682+class UbuntuSSOClientTestCase(BasicTestCase):
3683+ """Basic setup and helper functions."""
3684+
3685+ gui_class = gui.UbuntuSSOClientGUI
3686+ kwargs = dict(app_name=APP_NAME, tc_uri=TC_URI, help_text=HELP_TEXT)
3687+
3688+ def setUp(self):
3689+ """Init."""
3690+ super(UbuntuSSOClientTestCase, self).setUp()
3691+ self.patch(dbus, 'SessionBus', FakedSessionBus)
3692+ self.patch(dbus, 'Interface', FakedSSOBackend)
3693+ self.patch(socket, 'gethostname', lambda: TOKEN_NAME)
3694+ self.pages = ('enter_details', 'processing', 'verify_email', 'success',
3695+ 'tc_browser', 'login', 'reset_email', 'reset_password')
3696+ self.ui = self.gui_class(**self.kwargs)
3697+
3698+ def tearDown(self):
3699+ """Clean up."""
3700+ self.ui.bus.callbacks = {}
3701+ self.ui = None
3702+ super(UbuntuSSOClientTestCase, self).tearDown()
3703+
3704+ def assert_entries_are_packed_to_ui(self, container_name, entries):
3705+ """Every entry is properly packed in the ui 'container_name'."""
3706+ msg = 'Entry "%s" must be packed in "%s" but is not.'
3707+ container = getattr(self.ui, container_name)
3708+ for kind in entries:
3709+ name = '%s_entry' % kind
3710+ entry = getattr(self.ui, name)
3711+ self.assertIsInstance(entry, gui.LabeledEntry)
3712+ self.assertIn(entry, container, msg % (name, container_name))
3713+
3714+ def assert_warnings_visibility(self, visible=False):
3715+ """Every warning label should be 'visible'."""
3716+ msg = '"%s" should be %svisible.'
3717+ warnings = filter(lambda name: 'warning' in name, self.ui.widgets)
3718+ for name in warnings:
3719+ widget = getattr(self.ui, name)
3720+ self.assertEqual(visible, widget.get_property('visible'),
3721+ msg % (name, '' if visible else 'not '))
3722+
3723+ def assert_correct_warning(self, label, message):
3724+ """Check that a warning is shown displaying 'message'."""
3725+ # warning label is visible
3726+ self.assertTrue(label.get_property('visible'))
3727+
3728+ # warning content is correct
3729+ actual = label.get_text()
3730+ self.assertEqual(actual, message)
3731+
3732+ # content color is correct
3733+ expected = gui.WARNING_TEXT_COLOR
3734+ actual = label.style.fg[gtk.STATE_NORMAL]
3735+ self.assertEqual(expected, actual) # until realized this will fail
3736+
3737+ def assert_pages_visibility(self, **kwargs):
3738+ """The pages has the correct visibility."""
3739+ for i in self.pages:
3740+ kwargs.setdefault(i, False)
3741+
3742+ msg = 'page %r must %sbe visible.'
3743+ for name, expected in kwargs.iteritems():
3744+ page = getattr(self.ui, '%s_vbox' % name)
3745+ actual = page.get_property('visible')
3746+ self.assertEqual(expected, actual,
3747+ msg % (name, '' if expected else 'not '))
3748+
3749+ def click_join_with_valid_data(self):
3750+ """Move to the next page after entering details."""
3751+ self.ui.on_captcha_generated(captcha_id=CAPTCHA_ID) # we have captcha!
3752+
3753+ self.ui.name_entry.set_text(NAME)
3754+ # match emails
3755+ self.ui.email1_entry.set_text(EMAIL)
3756+ self.ui.email2_entry.set_text(EMAIL)
3757+ # match passwords
3758+ self.ui.password1_entry.set_text(PASSWORD)
3759+ self.ui.password2_entry.set_text(PASSWORD)
3760+ # agree to TC
3761+ self.ui.yes_to_tc_checkbutton.set_active(True)
3762+ # resolve captcha properly
3763+ self.ui.captcha_solution_entry.set_text(CAPTCHA_SOLUTION)
3764+
3765+ self.ui.join_ok_button.clicked()
3766+
3767+ def click_verify_email_with_valid_data(self):
3768+ """Move to the next page after entering email token."""
3769+ self.click_join_with_valid_data()
3770+
3771+ # resolve email token properly
3772+ self.ui.email_token_entry.set_text(EMAIL_TOKEN)
3773+
3774+ self.ui.verify_token_button.clicked()
3775+
3776+ def click_connect_with_valid_data(self):
3777+ """Move to the next page."""
3778+ # enter email
3779+ self.ui.login_email_entry.set_text(EMAIL)
3780+ # enter password
3781+ self.ui.login_password_entry.set_text(PASSWORD)
3782+
3783+ self.ui.login_ok_button.clicked()
3784+
3785+
3786+class BasicUbuntuSSOClientTestCase(UbuntuSSOClientTestCase):
3787+ """Test suite for basic functionality."""
3788+
3789+ def test_main_window_is_visible_at_startup(self):
3790+ """The main window is shown at startup."""
3791+ self.assertTrue(self.ui.window.get_property('visible'))
3792+
3793+ def test_closing_main_window_calls_close_callback(self):
3794+ """The close_callback is called when closing the main window."""
3795+ self.ui.close_callback = self._set_called
3796+ self.ui.on_close_clicked()
3797+ self.assertTrue(self._called,
3798+ 'close_callback was called when window was closed.')
3799+
3800+ def test_app_name_is_stored(self):
3801+ """The app_name is stored for further use."""
3802+ self.assertIn(APP_NAME, self.ui.app_name)
3803+
3804+ def test_session_bus_is_correct(self):
3805+ """The session bus is created and is correct."""
3806+ self.assertIsInstance(self.ui.bus, FakedSessionBus)
3807+
3808+ def test_iface_name_is_correct(self):
3809+ """The session bus is created and is correct."""
3810+ self.assertEqual(self.ui.iface_name, gui.DBUS_IFACE_USER_NAME)
3811+
3812+ def test_bus_object_is_created(self):
3813+ """SessionBus.get_object is called properly."""
3814+ self.assertIsInstance(self.ui.bus.obj, FakedDbusObject)
3815+ self.assertEqual(self.ui.bus.obj._args, ())
3816+ expected = dict(object_path=gui.DBUS_PATH,
3817+ bus_name=gui.DBUS_BUS_NAME,
3818+ follow_name_owner_changes=True)
3819+ self.assertEqual(expected, self.ui.bus.obj._kwargs)
3820+
3821+ def test_bus_interface_is_created(self):
3822+ """dbus.Interface is called properly."""
3823+ self.assertIsInstance(self.ui.backend, FakedSSOBackend)
3824+ self.assertEqual(self.ui.backend._args, ())
3825+ expected = dict(object=self.ui.bus.obj,
3826+ dbus_interface=gui.DBUS_IFACE_USER_NAME)
3827+ self.assertEqual(expected, self.ui.backend._kwargs)
3828+
3829+ def test_dbus_signals_are_removed(self):
3830+ """The hooked signals are removed at shutdown time."""
3831+ self.ui.bus.add_signal_receiver(gui.NO_OP, 'com.iface', 'signal_test')
3832+ assert len(self.ui.bus.callbacks) > 0 # at least one callback
3833+
3834+ self.ui.on_close_clicked()
3835+
3836+ self.assertEqual(self.ui.bus.callbacks, {})
3837+
3838+ def test_close_callback(self):
3839+ """A close_callback parameter is called when closing the window."""
3840+ ui = self.gui_class(close_callback=self._set_called, **self.kwargs)
3841+ ui.on_close_clicked()
3842+ self.assertTrue(self._called, 'close_callback was called on close.')
3843+
3844+ def test_close_callback_if_none(self):
3845+ """A close_callback parameter is not called if is None."""
3846+ ui = self.gui_class(close_callback=None, **self.kwargs)
3847+ ui.on_close_clicked()
3848+ # no crash when close_callback is None
3849+
3850+ def test_pages_are_packed_into_window(self):
3851+ """All the pages are packed in the main window."""
3852+ children = self.ui.window.get_children()[0].get_children()
3853+ for page_name in self.pages:
3854+ page = getattr(self.ui, '%s_vbox' % page_name)
3855+ self.assertIn(page, children)
3856+
3857+ def test_initial_text_for_entries(self):
3858+ """Entries have the correct text at startup."""
3859+ msg = 'Text for "%s" must be "%s" (got "%s" instead).'
3860+ for name in self.ui.entries:
3861+ entry = getattr(self.ui, name)
3862+ expected = getattr(self.ui, name.upper())
3863+ actual = entry.label
3864+ # text content is correct
3865+ self.assertEqual(expected, actual, msg % (name, expected, actual))
3866+
3867+ def test_password_fields_are_password(self):
3868+ msg = '"%s" should be a password LabeledEntry instance.'
3869+ passwords = filter(lambda name: 'password' in name,
3870+ self.ui.entries)
3871+ for name in passwords:
3872+ widget = getattr(self.ui, name)
3873+ self.assertTrue(widget.is_password, msg % name)
3874+
3875+ def test_warning_fields_are_hidden(self):
3876+ """Every warning label should be not visible."""
3877+ self.assert_warnings_visibility(visible=False)
3878+
3879+ def test_cancel_buttons_close_window(self):
3880+ """Every cancel button should close the window when clicked."""
3881+ msg = '"%s" should close the window when clicked.'
3882+ buttons = filter(lambda name: 'cancel_button' in name or
3883+ 'close_button' in name, self.ui.widgets)
3884+ for name in buttons:
3885+ self.ui = self.gui_class(close_callback=self._set_called,
3886+ **self.kwargs)
3887+ widget = getattr(self.ui, name)
3888+ widget.clicked()
3889+ self.assertEqual(self._called, ((widget,), {}), msg % name)
3890+ self._called = False
3891+
3892+ def test_window_icon(self):
3893+ """Main window has the proper icon."""
3894+ self.assertEqual('ubuntu-logo', self.ui.window.get_icon_name())
3895+
3896+ def test_every_cancel_emits_proper_signal(self):
3897+ """Clicking on any cancel button, 'user-cancelation' signal is sent."""
3898+ msg = 'user-cancelation is emitted when "%s" is clicked.'
3899+ buttons = filter(lambda name: 'cancel_button' in name, self.ui.widgets)
3900+ for name in buttons:
3901+ self.ui = self.gui_class(**self.kwargs)
3902+ self.ui.connect(gui.SIG_USER_CANCELATION, self._set_called)
3903+ widget = getattr(self.ui, name)
3904+ widget.clicked()
3905+ self.assertEqual(self._called, ((self.ui.window,), {}), msg % name)
3906+ self._called = False
3907+
3908+ def test_transient_window_is_None_if_window_id_is_zero(self):
3909+ """The transient window is correct."""
3910+ self.patch(gtk.gdk, 'window_foreign_new', self._set_called)
3911+ self.gui_class(window_id=0, **self.kwargs)
3912+ self.assertFalse(self._called, 'set_transient_for must not be called.')
3913+
3914+ def test_transient_window_is_correct(self):
3915+ """The transient window is correct."""
3916+ xid = 5
3917+ self.patch(gtk.gdk, 'window_foreign_new', self._set_called)
3918+ self.gui_class(window_id=xid, **self.kwargs)
3919+ self.assertTrue(self.memento.check(logging.ERROR, 'set_transient_for'))
3920+ self.assertTrue(self.memento.check(logging.ERROR, str(xid)))
3921+ self.assertEqual(self._called, ((xid,), {}))
3922+
3923+ def test_transient_window_accepts_negative_id(self):
3924+ """The transient window accepts a negative window id."""
3925+ xid = -5
3926+ self.patch(gtk.gdk, 'window_foreign_new', self._set_called)
3927+ self.gui_class(window_id=xid, **self.kwargs)
3928+ self.assertEqual(self._called, ((xid,), {}))
3929+
3930+
3931+class RegistrationEnterDetailsTestCase(UbuntuSSOClientTestCase):
3932+ """Test suite for the user registration (enter details page)."""
3933+
3934+ def test_initial_text_for_header_label(self):
3935+ """The header must have the correct text at startup."""
3936+ msg = 'Text for the header must be "%s" (got "%s" instead).'
3937+ expected = self.ui.JOIN_HEADER_LABEL % APP_NAME
3938+ actual = self.ui.header_label.get_text()
3939+ # text content is correct
3940+ self.assertEqual(expected, actual, msg % (expected, actual))
3941+
3942+ def test_entries_are_packed_to_ui(self):
3943+ """Every entry is properly packed in the ui."""
3944+ for kind in ('email', 'password'):
3945+ container_name = '%ss_hbox' % kind
3946+ entries = ('%s%s' % (kind, i) for i in xrange(1, 3))
3947+ self.assert_entries_are_packed_to_ui(container_name, entries)
3948+
3949+ self.assert_entries_are_packed_to_ui('enter_details_vbox', ('name',))
3950+ self.assert_entries_are_packed_to_ui('captcha_solution_vbox',
3951+ ('captcha_solution',))
3952+ self.assert_entries_are_packed_to_ui('verify_email_vbox',
3953+ ('email_token',))
3954+
3955+ def test_initial_texts_for_checkbuttons(self):
3956+ """Check buttons have the correct text at startup."""
3957+ msg = 'Text for "%s" must be "%s" (got "%s" instead).'
3958+ expected = self.ui.YES_TO_UPDATES
3959+ actual = self.ui.yes_to_updates_checkbutton.get_label()
3960+ self.assertEqual(expected, actual, msg % ('yes_to_updates_checkbutton',
3961+ expected, actual))
3962+ expected = self.ui.YES_TO_TC
3963+ actual = self.ui.yes_to_tc_checkbutton.get_label()
3964+ self.assertEqual(expected, actual,
3965+ msg % ('yes_to_tc_checkbutton', expected, actual))
3966+
3967+ def test_checkbuttons_are_checked_at_startup(self):
3968+ """Checkbuttons are checked by default."""
3969+ msg = '"%s" is checked by default.'
3970+ for name in ('yes_to_updates_checkbutton', 'yes_to_tc_checkbutton'):
3971+ widget = getattr(self.ui, name)
3972+ self.assertTrue(widget.get_active(), msg % name)
3973+
3974+ def test_vboxes_visible_properties(self):
3975+ """Only 'enter_details' vbox is visible at start up."""
3976+ self.assert_pages_visibility(enter_details=True)
3977+
3978+ def test_join_ok_button_clicked(self):
3979+ """Clicking 'join_ok_button' sends info to backend using 'register'."""
3980+ self.click_join_with_valid_data()
3981+
3982+ # assert register_user was called
3983+ expected = 'register_user'
3984+ self.assertIn(expected, self.ui.backend._called)
3985+ self.assertEqual(self.ui.backend._called[expected],
3986+ ((EMAIL, PASSWORD, CAPTCHA_ID, CAPTCHA_SOLUTION),
3987+ dict(reply_handler=gui.NO_OP,
3988+ error_handler=gui.NO_OP)))
3989+
3990+ def test_join_ok_button_clicked_morphs_to_processing_page(self):
3991+ """Clicking 'join_ok_button' presents the processing vbox."""
3992+ self.click_join_with_valid_data()
3993+ self.assert_pages_visibility(processing=True)
3994+
3995+ def test_processing_vbox_displays_an_active_spinner(self):
3996+ """When processing the registration, an active spinner is shown."""
3997+ self.click_join_with_valid_data()
3998+
3999+ self.assertTrue(self.ui.processing_vbox.get_property('visible'),
4000+ 'the processing box should be visible.')
4001+
4002+ box = self.ui.processing_vbox.get_children()[0].get_children()[0]
4003+ self.assertEqual(2, len(box.get_children()),
4004+ 'processing_vbox must have two children.')
4005+
4006+ spinner, label = box.get_children()
4007+ self.assertIsInstance(spinner, gtk.Spinner)
4008+ self.assertIsInstance(label, gtk.Label)
4009+
4010+ self.assertTrue(spinner.get_property('visible'),
4011+ 'the processing spinner should be visible.')
4012+ self.assertTrue(spinner.get_property('active'),
4013+ 'the processing spinner should be active.')
4014+ self.assertTrue(label.get_property('visible'),
4015+ 'the processing label should be visible.')
4016+ self.assertEqual(label.get_text(), self.ui.ONE_MOMENT_PLEASE,
4017+ 'the processing label text must be correct.')
4018+
4019+ def test_captcha_image_is_not_visible_at_startup(self):
4020+ """Captcha image is not shown at startup."""
4021+ self.assertFalse(self.ui.captcha_image.get_property('visible'),
4022+ 'the captcha_image should not be visible.')
4023+
4024+ def test_captcha_filename_is_different_each_time(self):
4025+ """The captcha image is different each time."""
4026+ ui = self.gui_class(**self.kwargs)
4027+ self.assertNotEqual(self.ui._captcha_filename, ui._captcha_filename)
4028+
4029+ def test_captcha_image_is_removed_when_exiting(self):
4030+ """The captcha image is removed at shutdown time."""
4031+ open(self.ui._captcha_filename, 'w').close()
4032+ assert os.path.exists(self.ui._captcha_filename)
4033+ self.ui.on_close_clicked()
4034+
4035+ self.assertFalse(os.path.exists(self.ui._captcha_filename),
4036+ 'captcha image must be removed when exiting.')
4037+
4038+ def test_captcha_image_is_a_spinner_at_first(self):
4039+ """Captcha image shows a spinner until the image is downloaded."""
4040+ self.assertTrue(self.ui.captcha_loading.get_property('visible'),
4041+ 'the captcha_loading box should be visible.')
4042+
4043+ box = self.ui.captcha_loading.get_children()[0].get_children()[0]
4044+ self.assertEqual(2, len(box.get_children()),
4045+ 'captcha_loading must have two children.')
4046+
4047+ spinner, label = box.get_children()
4048+ self.assertIsInstance(spinner, gtk.Spinner)
4049+ self.assertIsInstance(label, gtk.Label)
4050+
4051+ self.assertTrue(spinner.get_property('visible'),
4052+ 'the captcha_loading spinner should be visible.')
4053+ self.assertTrue(spinner.get_property('active'),
4054+ 'the captcha_loading spinner should be active.')
4055+ self.assertTrue(label.get_property('visible'),
4056+ 'the captcha_loading label should be visible.')
4057+ self.assertEqual(label.get_text(), self.ui.LOADING,
4058+ 'the captcha_loading label text must be correct.')
4059+
4060+ def test_join_ok_button_is_disabled_until_captcha_is_available(self):
4061+ """The join_ok_button is not sensitive until captcha is available."""
4062+ self.assertFalse(self.ui.join_ok_button.is_sensitive())
4063+
4064+ def test_join_ok_button_is_enabled_when_captcha_is_available(self):
4065+ """The join_ok_button is sensitive when captcha is available."""
4066+ self.ui.on_captcha_generated(captcha_id=CAPTCHA_ID)
4067+ self.assertTrue(self.ui.join_ok_button.is_sensitive())
4068+
4069+ def test_captcha_loading_is_hid_when_captcha_is_available(self):
4070+ """The captcha_loading is hid when captcha is available."""
4071+ self.ui.on_captcha_generated(captcha_id=CAPTCHA_ID)
4072+ self.assertFalse(self.ui.captcha_loading.get_property('visible'),
4073+ 'captcha_loading is not visible.')
4074+
4075+ def test_captcha_id_is_stored_when_captcha_is_available(self):
4076+ """The captcha_id is stored when captcha is available."""
4077+ self.ui.on_captcha_generated(captcha_id=CAPTCHA_ID)
4078+ self.assertEqual(CAPTCHA_ID, self.ui._captcha_id)
4079+
4080+ def test_captcha_image_is_requested_as_startup(self):
4081+ """The captcha image is requested at startup."""
4082+ # assert generate_captcha was called
4083+ expected = 'generate_captcha'
4084+ self.assertIn(expected, self.ui.backend._called)
4085+ self.assertEqual(self.ui.backend._called[expected],
4086+ ((self.ui._captcha_filename,),
4087+ dict(reply_handler=gui.NO_OP,
4088+ error_handler=gui.NO_OP)))
4089+
4090+ def test_captcha_is_shown_when_available(self):
4091+ """The captcha image is shown when available."""
4092+ self.patch(self.ui.captcha_image, 'set_from_file', self._set_called)
4093+ self.ui.on_captcha_generated(captcha_id=CAPTCHA_ID)
4094+ self.assertTrue(self.ui.captcha_image.get_property('visible'))
4095+ self.assertEqual(self._called, ((self.ui._captcha_filename,), {}))
4096+
4097+ def test_login_button_has_correct_wording(self):
4098+ """The sign in button has the proper wording."""
4099+ actual = self.ui.login_button.get_label()
4100+ self.assertEqual(self.ui.LOGIN_BUTTON_LABEL, actual)
4101+
4102+
4103+class TermsAndConditionsTestCase(UbuntuSSOClientTestCase):
4104+ """Test suite for the user registration (terms & conditions page)."""
4105+
4106+ def test_tc_button_clicked_morphs_into_tc_browser_vbox(self):
4107+ """Terms & Conditions morphs to a browser window."""
4108+ self.ui.tc_button.clicked()
4109+ self.assert_pages_visibility(tc_browser=True)
4110+
4111+ def test_tc_back_clicked_returns_to_previous_page(self):
4112+ """Terms & Conditions back button return to previous page."""
4113+ self.ui.tc_button.clicked()
4114+ self.ui.tc_back_button.clicked()
4115+ self.assert_pages_visibility(enter_details=True)
4116+
4117+ def test_tc_button_has_the_proper_wording(self):
4118+ """Terms & Conditions has the proper wording."""
4119+ self.assertEqual(self.ui.tc_button.get_label(), self.ui.TC)
4120+
4121+ def test_tc_browser_opens_the_proper_uri(self):
4122+ """Terms & Conditions browser shows the proper uri."""
4123+ self.ui.tc_button.clicked()
4124+ self.assertEqual(self.ui.tc_browser.get_property('uri'), TC_URI)
4125+ test_tc_browser_opens_the_proper_uri.skip = 'The freaking test wont work.'
4126+
4127+
4128+class UserRegistrationErrorTestCase(UbuntuSSOClientTestCase):
4129+ """Test suite for the user registration error handling."""
4130+
4131+ def setUp(self):
4132+ """Init."""
4133+ super(UserRegistrationErrorTestCase, self).setUp()
4134+ self.click_join_with_valid_data()
4135+ self.ui.on_user_registration_error()
4136+
4137+ def test_previous_page_is_shown(self):
4138+ """On UserRegistrationError the previous page is shown."""
4139+ self.assert_pages_visibility(enter_details=True)
4140+
4141+ def test_warning_label_is_shown(self):
4142+ """On UserRegistrationError the warning label is shown."""
4143+ self.assert_correct_warning(self.ui.warning_label,
4144+ self.ui.UNKNOWN_ERROR)
4145+
4146+ def test_proper_signal_is_emitted(self):
4147+ """On UserRegistrationError, 'registration-failed' signal is sent."""
4148+ self.ui.connect(gui.SIG_REGISTRATION_FAILED, self._set_called)
4149+ self.ui.on_user_registration_error()
4150+ self.assertEqual(((self.ui.window,), {}), self._called)
4151+
4152+
4153+class VerifyEmailTestCase(UbuntuSSOClientTestCase):
4154+ """Test suite for the user registration (verify email page)."""
4155+
4156+ def test_verify_email_image_is_correct(self):
4157+ """The email-example image is shown."""
4158+ pixbuf = self.ui.verify_email_image.get_pixbuf()
4159+ self.assertTrue(pixbuf is not None)
4160+ self.assertEqual(self.ui._email_example_pixbuf, pixbuf)
4161+
4162+ def test_registration_successful_shows_verify_email_vbox(self):
4163+ """Receiving 'registration_successful' shows the verify email vbox."""
4164+ self.ui.on_user_registered()
4165+ self.assert_pages_visibility(verify_email=True)
4166+
4167+ def test_help_label_display_correct_wording(self):
4168+ """The help_label display VERIFY_EMAIL_LABEL."""
4169+ self.ui.on_user_registered()
4170+ msg = 'help_label must read "%s" (got "%s" instead).'
4171+ actual = self.ui.help_label.get_text()
4172+ expected = self.ui.VERIFY_EMAIL_LABEL
4173+ self.assertEqual(expected, actual, msg % (expected, actual))
4174+
4175+ def test_on_verify_token_button_clicked_calls_validate_email(self):
4176+ """Verify token button triggers call to backend."""
4177+ self.click_verify_email_with_valid_data()
4178+ expected = 'validate_email'
4179+ self.assertIn(expected, self.ui.backend._called)
4180+ self.assertEqual(self.ui.backend._called[expected],
4181+ ((EMAIL, PASSWORD, APP_NAME, EMAIL_TOKEN),
4182+ dict(reply_handler=gui.NO_OP,
4183+ error_handler=gui.NO_OP)))
4184+
4185+ def test_on_verify_token_button_shows_processing_page(self):
4186+ """Verify token button triggers call to backend."""
4187+ self.click_verify_email_with_valid_data()
4188+ self.assert_pages_visibility(processing=True)
4189+
4190+ def test_no_warning_messages_if_valid_data_on_verify_token(self):
4191+ """No warning messages are shown if the data is valid."""
4192+ # this will certainly NOT generate warnings
4193+ self.click_verify_email_with_valid_data()
4194+ self.assert_warnings_visibility(visible=False)
4195+
4196+ def test_on_email_validated_shows_success_page(self):
4197+ """On email validated the success page is shown."""
4198+ self.ui.on_email_validated(APP_NAME)
4199+ self.assert_pages_visibility(success=True)
4200+
4201+ def test_on_email_validated_clears_the_help_text(self):
4202+ """On email validated the help text is removed."""
4203+ self.ui.on_email_validated(APP_NAME)
4204+ self.assertEqual('', self.ui.help_label.get_text())
4205+ test_on_email_validated_clears_the_help_text.skip = 'Maybe this is wrong.'
4206+
4207+ def test_on_email_validated_proper_signals_is_emitted(self):
4208+ """On email validated, 'registration-succeeded' signal is sent."""
4209+ self.ui.connect(gui.SIG_REGISTRATION_SUCCEEDED, self._set_called)
4210+ self.ui.on_email_validated(APP_NAME)
4211+ self.assertEqual(((self.ui.window, APP_NAME), {}), self._called)
4212+
4213+ def test_on_email_validation_error_proper_signals_is_emitted(self):
4214+ """On email validation error, 'registration-failed' signal is sent."""
4215+ self.ui.connect(gui.SIG_REGISTRATION_FAILED, self._set_called)
4216+ self.ui.on_email_validation_error()
4217+ self.assertEqual(((self.ui.window,), {}), self._called)
4218+
4219+ def test_success_label_is_correct(self):
4220+ """The success message is correct."""
4221+ self.assertEqual(self.ui.SUCCESS, self.ui.success_label.get_text())
4222+ markup = self.ui.success_label.get_label()
4223+ self.assertTrue('<span size="x-large">' in markup)
4224+
4225+ def test_on_success_close_button_clicked_closes_window(self):
4226+ """When done the window is closed."""
4227+ self.ui.success_close_button.clicked()
4228+ self.assertFalse(self.ui.window.get_property('visible'))
4229+
4230+
4231+class RegistrationValidationTestCase(UbuntuSSOClientTestCase):
4232+ """Test suite for the user registration validations."""
4233+
4234+ def test_warning_is_shown_if_name_empty(self):
4235+ """A warning message is shown if name is empty."""
4236+ self.ui.name_entry.set_text('')
4237+
4238+ self.ui.join_ok_button.clicked() # submit form
4239+
4240+ self.assert_correct_warning(self.ui.name_warning_label,
4241+ self.ui.FIELD_REQUIRED)
4242+ self.assertNotIn('register_user', self.ui.backend._called)
4243+
4244+ def test_warning_is_shown_if_empty_email(self):
4245+ """A warning message is shown if emails are empty."""
4246+ self.ui.email1_entry.set_text('')
4247+ self.ui.email2_entry.set_text('')
4248+
4249+ self.ui.join_ok_button.clicked() # submit form
4250+
4251+ self.assert_correct_warning(self.ui.email_warning_label,
4252+ self.ui.FIELD_REQUIRED)
4253+ self.assertNotIn('register_user', self.ui.backend._called)
4254+
4255+ def test_warning_is_shown_if_email_mismatch(self):
4256+ """A warning message is shown if emails doesn't match."""
4257+ self.ui.email1_entry.set_text(EMAIL)
4258+ self.ui.email2_entry.set_text(EMAIL * 2)
4259+
4260+ self.ui.join_ok_button.clicked() # submit form
4261+
4262+ self.assert_correct_warning(self.ui.email_warning_label,
4263+ self.ui.EMAIL_MISMATCH)
4264+ self.assertNotIn('register_user', self.ui.backend._called)
4265+
4266+ def test_warning_is_shown_if_invalid_email(self):
4267+ """A warning message is shown if email is invalid."""
4268+ self.ui.email1_entry.set_text('q')
4269+ self.ui.email2_entry.set_text('q')
4270+
4271+ self.ui.join_ok_button.clicked() # submit form
4272+
4273+ self.assert_correct_warning(self.ui.email_warning_label,
4274+ self.ui.EMAIL_INVALID)
4275+ self.assertNotIn('register_user', self.ui.backend._called)
4276+
4277+ def test_password_help_is_always_shown(self):
4278+ """Password help text is correctly displayed."""
4279+ self.assertTrue(self.ui.password_help_label.get_property('visible'),
4280+ 'password help text is visible.')
4281+ self.assertEqual(self.ui.password_help_label.get_text(),
4282+ self.ui.PASSWORD_HELP)
4283+ self.assertNotIn('register_user', self.ui.backend._called)
4284+
4285+ def test_warning_is_shown_if_password_mismatch(self):
4286+ """A warning message is shown if password doesn't match."""
4287+ self.ui.password1_entry.set_text(PASSWORD)
4288+ self.ui.password2_entry.set_text(PASSWORD * 2)
4289+
4290+ self.ui.join_ok_button.clicked() # submit form
4291+
4292+ self.assert_correct_warning(self.ui.password_warning_label,
4293+ self.ui.PASSWORD_MISMATCH)
4294+ self.assertNotIn('register_user', self.ui.backend._called)
4295+
4296+ def test_warning_is_shown_if_password_too_weak(self):
4297+ """A warning message is shown if password is too weak."""
4298+ # password will match but will be too weak
4299+ for w in ('', 'h3lloWo', PASSWORD.lower(), 'helloWorld'):
4300+ self.ui.password1_entry.set_text(w)
4301+ self.ui.password2_entry.set_text(w)
4302+
4303+ self.ui.join_ok_button.clicked() # submit form
4304+
4305+ self.assert_correct_warning(self.ui.password_warning_label,
4306+ self.ui.PASSWORD_TOO_WEAK)
4307+ self.assertNotIn('register_user', self.ui.backend._called)
4308+
4309+ def test_warning_is_shown_if_tc_not_accepted(self):
4310+ """A warning message is shown if TC are not accepted."""
4311+ # don't agree to TC
4312+ self.ui.yes_to_tc_checkbutton.set_active(False)
4313+
4314+ self.ui.join_ok_button.clicked() # submit form
4315+
4316+ self.assert_correct_warning(self.ui.tc_warning_label,
4317+ self.ui.TC_NOT_ACCEPTED)
4318+ self.assertNotIn('register_user', self.ui.backend._called)
4319+
4320+ def test_warning_is_shown_if_not_captcha_solution(self):
4321+ """A warning message is shown if TC are not accepted."""
4322+ # captcha solution will be empty
4323+ self.ui.captcha_solution_entry.set_text('')
4324+
4325+ self.ui.join_ok_button.clicked() # submit form
4326+
4327+ self.assert_correct_warning(self.ui.captcha_solution_warning_label,
4328+ self.ui.FIELD_REQUIRED)
4329+ self.assertNotIn('register_user', self.ui.backend._called)
4330+
4331+ def test_no_warning_messages_if_valid_data_on_enter_details(self):
4332+ """No warning messages are shown if the data is valid."""
4333+ # this will certainly NOT generate warnings
4334+ self.click_join_with_valid_data()
4335+
4336+ self.assert_warnings_visibility(visible=False)
4337+
4338+ def test_no_warning_messages_if_valid_data_after_invalid_data(self):
4339+ """No warnings if the data is valid (with prior invalid data)."""
4340+ # this will certainly generate warnings
4341+ self.ui.join_ok_button.clicked()
4342+
4343+ # this will certainly NOT generate warnings
4344+ self.click_join_with_valid_data()
4345+
4346+ self.assert_warnings_visibility(visible=False)
4347+
4348+
4349+class LoginTestCase(UbuntuSSOClientTestCase):
4350+ """Test suite for the user login pages."""
4351+
4352+ def setUp(self):
4353+ """Init."""
4354+ super(LoginTestCase, self).setUp()
4355+ self.ui.login_button.clicked()
4356+
4357+ def test_login_button_clicked_morphs_to_login_page(self):
4358+ """Clicking sig_in_button morphs window into login page."""
4359+ self.assert_pages_visibility(login=True)
4360+
4361+ def test_initial_text_for_header_label(self):
4362+ """The header must have the correct text when logging in."""
4363+ msg = 'Text for the header must be "%s" (got "%s" instead).'
4364+ expected = self.ui.LOGIN_HEADER_LABEL % APP_NAME
4365+ actual = self.ui.header_label.get_text()
4366+ self.assertEqual(expected, actual, msg % (expected, actual))
4367+
4368+ def test_initial_text_for_help_label(self):
4369+ """The help must have the correct text at startup."""
4370+ msg = 'Text for the help must be "%s" (got "%s" instead).'
4371+ expected = self.ui.CONNECT_HELP_LABEL % APP_NAME
4372+ actual = self.ui.help_label.get_text()
4373+ self.assertEqual(expected, actual, msg % (expected, actual))
4374+
4375+ def test_entries_are_packed_to_ui_for_login(self):
4376+ """Every entry is properly packed in the ui for the login page."""
4377+ entries = ('login_email', 'login_password')
4378+ self.assert_entries_are_packed_to_ui('login_details_vbox', entries)
4379+
4380+ def test_entries_are_packed_to_ui_for_reset_password(self):
4381+ """Every entry is packed in the ui for the reset password page."""
4382+ entries = ('reset_code', 'reset_password1', 'reset_password2')
4383+ self.assert_entries_are_packed_to_ui('reset_password_details_vbox',
4384+ entries)
4385+
4386+ def test_entries_are_packed_to_ui_for_reset_email(self):
4387+ """Every entry is packed in the ui for the reset email page."""
4388+ entries = ('reset_email',)
4389+ self.assert_entries_are_packed_to_ui('reset_email_details_vbox',
4390+ entries)
4391+
4392+ def test_on_login_back_button_clicked(self):
4393+ """Clicking login_back_button show registration screen."""
4394+ self.ui.login_back_button.clicked()
4395+ self.assert_pages_visibility(enter_details=True)
4396+
4397+ def test_on_login_connect_button_clicked(self):
4398+ """Clicking login_ok_button calls backend.login."""
4399+ self.click_connect_with_valid_data()
4400+
4401+ expected = 'login'
4402+ self.assertIn(expected, self.ui.backend._called)
4403+ self.assertEqual(self.ui.backend._called[expected],
4404+ ((EMAIL, PASSWORD, APP_NAME),
4405+ dict(reply_handler=gui.NO_OP,
4406+ error_handler=gui.NO_OP)))
4407+
4408+ def test_on_login_connect_button_clicked_morphs_to_processing_page(self):
4409+ """Clicking login_ok_button morphs to the processing page."""
4410+ self.click_connect_with_valid_data()
4411+ self.assert_pages_visibility(processing=True)
4412+
4413+ def test_on_logged_in_proper_signals_is_emitted(self):
4414+ """On user logged in, 'login-succeeded' signal is sent."""
4415+ self.ui.connect(gui.SIG_LOGIN_SUCCEEDED, self._set_called)
4416+ self.ui.on_logged_in(APP_NAME)
4417+ self.assertEqual(((self.ui.window, APP_NAME), {}), self._called)
4418+
4419+ def test_on_logged_in_morphs_to_success_page(self):
4420+ """On user logged in, the success page is shown."""
4421+ self.click_connect_with_valid_data()
4422+ self.ui.on_logged_in(APP_NAME)
4423+ self.assert_pages_visibility(success=True)
4424+
4425+ def test_on_login_error_morphs_to_login_page(self):
4426+ """On user login error, the previous page is shown."""
4427+ self.click_connect_with_valid_data()
4428+ self.ui.on_login_error()
4429+ self.assert_pages_visibility(login=True)
4430+
4431+ def test_on_login_error_a_warning_is_shown(self):
4432+ """On user login error, a warning is shown with proper wording."""
4433+ self.click_connect_with_valid_data()
4434+ self.ui.on_login_error()
4435+ self.assert_correct_warning(self.ui.warning_label,
4436+ self.ui.UNKNOWN_ERROR)
4437+
4438+ def test_back_to_registration_hides_warning(self):
4439+ """After user login error, warning is hidden when clicking 'Back'."""
4440+ self.click_connect_with_valid_data()
4441+ self.ui.on_login_error()
4442+ self.ui.login_back_button.clicked()
4443+ self.assertFalse(self.ui.warning_label.get_property('visible'))
4444+
4445+ def test_on_email_validation_error_proper_signals_is_emitted(self):
4446+ """On user login error, 'login-failed' signal is sent."""
4447+ self.ui.connect(gui.SIG_LOGIN_FAILED, self._set_called)
4448+ self.click_connect_with_valid_data()
4449+ self.ui.on_login_error()
4450+ self.assertEqual(((self.ui.window,), {}), self._called)
4451+
4452+
4453+class LoginValidationTestCase(UbuntuSSOClientTestCase):
4454+ """Test suite for the user login validation."""
4455+
4456+ def setUp(self):
4457+ """Init."""
4458+ super(LoginValidationTestCase, self).setUp()
4459+ self.ui.login_button.clicked()
4460+
4461+ def test_warning_is_shown_if_empty_email(self):
4462+ """A warning message is shown if email is empty."""
4463+ self.ui.login_email_entry.set_text('')
4464+
4465+ self.ui.login_ok_button.clicked() # submit form
4466+
4467+ self.assert_correct_warning(self.ui.login_email_warning_label,
4468+ self.ui.FIELD_REQUIRED)
4469+ self.assertNotIn('login', self.ui.backend._called)
4470+
4471+ def test_warning_is_shown_if_invalid_email(self):
4472+ """A warning message is shown if email is invalid."""
4473+ self.ui.login_email_entry.set_text('q')
4474+
4475+ self.ui.login_ok_button.clicked() # submit form
4476+
4477+ self.assert_correct_warning(self.ui.login_email_warning_label,
4478+ self.ui.EMAIL_INVALID)
4479+ self.assertNotIn('login', self.ui.backend._called)
4480+
4481+ def test_warning_is_shown_if_empty_password(self):
4482+ """A warning message is shown if password is empty."""
4483+ self.ui.login_password_entry.set_text('')
4484+
4485+ self.ui.login_ok_button.clicked() # submit form
4486+
4487+ self.assert_correct_warning(self.ui.login_password_warning_label,
4488+ self.ui.FIELD_REQUIRED)
4489+ self.assertNotIn('login', self.ui.backend._called)
4490+
4491+ def test_no_warning_messages_if_valid_data(self):
4492+ """No warning messages are shown if the data is valid."""
4493+ # this will certainly NOT generate warnings
4494+ self.click_connect_with_valid_data()
4495+
4496+ self.assert_warnings_visibility(visible=False)
4497+
4498+ def test_no_warning_messages_if_valid_data_after_invalid_data(self):
4499+ """No warnings if the data is valid (with prior invalid data)."""
4500+ # this will certainly generate warnings
4501+ self.ui.login_ok_button.clicked()
4502+
4503+ # this will certainly NOT generate warnings
4504+ self.click_connect_with_valid_data()
4505+
4506+ self.assert_warnings_visibility(visible=False)
4507+
4508+
4509+class ResetPasswordTestCase(UbuntuSSOClientTestCase):
4510+ """Test suite for the reset password functionality."""
4511+
4512+ def setUp(self):
4513+ """Init."""
4514+ super(ResetPasswordTestCase, self).setUp()
4515+ self.ui.login_button.clicked()
4516+ self.ui.forgotten_password_button.clicked()
4517+
4518+ def test_forgotten_password_button_has_the_proper_wording(self):
4519+ """The forgotten_password_button has the proper wording."""
4520+ self.assertEqual(self.ui.forgotten_password_button.get_label(),
4521+ self.ui.FORGOTTEN_PASSWORD_BUTTON)
4522+
4523+ def test_on_forgotten_password_button_clicked_help_text(self):
4524+ """Clicking forgotten_password_button the help is properly changed."""
4525+ self.assertEqual(self.ui.help_label.get_text(),
4526+ self.ui.RESET_EMAIL_LABEL)
4527+
4528+ def test_on_forgotten_password_button_clicked_header_label(self):
4529+ """Clicking forgotten_password_button the title is properly changed."""
4530+ self.assertEqual(self.ui.header_label.get_text(),
4531+ self.ui.RESET_PASSWORD)
4532+
4533+ def test_on_forgotten_password_button_clicked_ok_button(self):
4534+ """Clicking forgotten_password_button the ok button reads 'Next'."""
4535+ self.assertEqual(self.ui.reset_email_ok_button.get_label(),
4536+ self.ui.NEXT)
4537+
4538+ def test_on_forgotten_password_button_clicked_morphs_window(self):
4539+ """Clicking forgotten_password_button the proper page is shown."""
4540+ self.assert_pages_visibility(reset_email=True)
4541+
4542+ def test_on_reset_email_back_button_clicked(self):
4543+ """Clicking reset_email_back_button show login screen."""
4544+ self.ui.reset_email_back_button.clicked()
4545+ self.assert_pages_visibility(login=True)
4546+
4547+ def test_on_reset_email_ok_button_disabled_until_email_added(self):
4548+ """The reset_email_ok_button is disabled until email added."""
4549+ self.assertFalse(self.ui.reset_email_ok_button.get_sensitive())
4550+
4551+ self.ui.reset_email_entry.set_text('a')
4552+ self.assertTrue(self.ui.reset_email_ok_button.get_sensitive())
4553+
4554+ self.ui.reset_email_entry.set_text('')
4555+ self.assertFalse(self.ui.reset_email_ok_button.get_sensitive())
4556+
4557+ self.ui.reset_email_entry.set_text(' ')
4558+ self.assertFalse(self.ui.reset_email_ok_button.get_sensitive())
4559+
4560+ def test_on_reset_email_ok_button_clicked_help_text(self):
4561+ """Clicking reset_email_ok_button the help is properly changed."""
4562+ expected = 'a@example.com'
4563+ self.ui.reset_email_entry.set_text(expected)
4564+ self.ui.reset_email_ok_button.clicked()
4565+
4566+ self.assertEqual(self.ui.help_label.get_text(),
4567+ self.ui.RESET_PASSWORD_LABEL % expected)
4568+
4569+ def test_on_reset_email_ok_button_clicked_ok_button(self):
4570+ """Clicking reset_email_ok_button the ok button reads 'Next'."""
4571+ self.ui.reset_email_ok_button.clicked()
4572+
4573+ self.assertEqual(self.ui.reset_password_ok_button.get_label(),
4574+ self.ui.RESET_PASSWORD)
4575+
4576+ def test_on_reset_email_ok_button_clicked_morphs_window(self):
4577+ """Clicking reset_email_ok_button the proper page is shown."""
4578+ self.ui.reset_email_ok_button.clicked()
4579+ self.assert_pages_visibility(reset_password=True)
4580+
4581+ def test_on_reset_password_ok_button_disabled(self):
4582+ """The reset_password_ok_button is disabled until values added."""
4583+ self.ui.reset_email_ok_button.clicked()
4584+ self.assertFalse(self.ui.reset_password_ok_button.get_sensitive())
4585+
4586+ msg = 'reset_password_ok_button must be sensitive(%s) for entries %r.'
4587+ entries = (self.ui.reset_code_entry,
4588+ self.ui.reset_password1_entry,
4589+ self.ui.reset_password2_entry)
4590+ for xs in itertools.product(('', ' ', 'a'), repeat=3):
4591+ expected = True
4592+ for entry, x in zip(entries, xs):
4593+ entry.set_text(x)
4594+ expected &= bool(x and not x.isspace())
4595+
4596+ actual = self.ui.reset_password_ok_button.get_sensitive()
4597+ self.assertEqual(expected, actual, msg % (expected, xs))
4598+
4599+
4600+class DbusTestCase(UbuntuSSOClientTestCase):
4601+ """Test suite for the dbus calls."""
4602+
4603+ def test_signal_receivers_are_connected(self):
4604+ """Callbacks are connected to signals of interest."""
4605+ msg1 = 'callback %r for signal %r must be added to the internal bus.'
4606+ msg2 = 'callback %r for signal %r must be added to the ui log.'
4607+ for dbus_iface, signal, method in self.ui._signals:
4608+ actual = self.ui.bus.callbacks.get((dbus_iface, signal))
4609+ self.assertEqual(method, actual, msg1 % (method, signal))
4610+ actual = self.ui._signals_receivers.get((dbus_iface, signal))
4611+ self.assertEqual(method, actual, msg2 % (method, signal))
4612
4613=== added file 'ubuntu_sso/tests/test_login.py'
4614--- ubuntu_sso/tests/test_login.py 1970-01-01 00:00:00 +0000
4615+++ ubuntu_sso/tests/test_login.py 2010-08-11 18:19:01 +0000
4616@@ -0,0 +1,188 @@
4617+# -*- coding: utf-8 -*-
4618+#
4619+# Author: Rodney Dawes <rodney.dawes@canonical.com>
4620+#
4621+# Copyright 2010 Canonical Ltd.
4622+#
4623+# This program is free software: you can redistribute it and/or modify it
4624+# under the terms of the GNU General Public License version 3, as published
4625+# by the Free Software Foundation.
4626+#
4627+# This program is distributed in the hope that it will be useful, but
4628+# WITHOUT ANY WARRANTY; without even the implied warranties of
4629+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
4630+# PURPOSE. See the GNU General Public License for more details.
4631+#
4632+# You should have received a copy of the GNU General Public License along
4633+# with this program. If not, see <http://www.gnu.org/licenses/>.
4634+""" Tests for the ubuntuone-login script """
4635+
4636+import dbus.service
4637+import new
4638+import os
4639+
4640+from contrib.testing.testcase import DBusTestCase, FakeLogin
4641+from twisted.internet import defer
4642+from twisted.python.failure import Failure
4643+
4644+
4645+class InvalidSignalError(Exception):
4646+ """Exception for when we get the wrong signal called."""
4647+ pass
4648+
4649+
4650+class LoginTests(DBusTestCase):
4651+ """Basic tests for the ubuntuone-login script """
4652+
4653+ _path = os.path.join(os.getcwd(), "bin", "ubuntu-sso-login")
4654+ u1login = new.module('u1login')
4655+ execfile(_path, u1login.__dict__)
4656+
4657+ def setUp(self):
4658+ DBusTestCase.setUp(self)
4659+ self.oauth = FakeLogin(self.bus)
4660+
4661+ def tearDown(self):
4662+ # collect all signal receivers registered during the test
4663+ signal_receivers = set()
4664+ with self.bus._signals_lock:
4665+ for group in self.bus._signal_recipients_by_object_path.values():
4666+ for matches in group.values():
4667+ for match in matches.values():
4668+ signal_receivers.update(match)
4669+ d = self.cleanup_signal_receivers(signal_receivers)
4670+
4671+ def shutdown(r):
4672+ """Shutdown."""
4673+ self.oauth.shutdown()
4674+
4675+ d.addBoth(shutdown)
4676+ d.addBoth(lambda _: DBusTestCase.tearDown(self))
4677+ return d
4678+
4679+ def main(self):
4680+ """ Override LoginMain.main """
4681+ return
4682+
4683+ def test_new_credentials(self):
4684+ """ Test logging in """
4685+ def new_creds(realm=None, consumer_key=None, sender=None):
4686+ """ Override the callback """
4687+ d.callback(True)
4688+
4689+ def auth_denied():
4690+ """ Override the callback """
4691+ d.errback(Failure(InvalidSignalError()))
4692+
4693+ def got_oauth_error(message=None):
4694+ """ Override the callback """
4695+ d.errback(Failure(InvalidSignalError()))
4696+
4697+ def set_up_desktopcouch_pairing(consumer_key):
4698+ """ Override the method """
4699+ return
4700+
4701+ login = self.u1login.LoginMain()
4702+ login.main = self.main
4703+ login.new_credentials = new_creds
4704+ login.auth_denied = auth_denied
4705+ login.got_oauth_error = got_oauth_error
4706+ login.set_up_desktopcouch_pairing = set_up_desktopcouch_pairing
4707+
4708+ login._connect_dbus_signals()
4709+
4710+ client = self.bus.get_object(self.u1login.DBUS_IFACE_AUTH_NAME,
4711+ '/oauthdesktop',
4712+ follow_name_owner_changes=True)
4713+ d = defer.Deferred()
4714+
4715+ def login_handler():
4716+ """ login handler """
4717+ return
4718+
4719+ iface = dbus.Interface(client, self.u1login.DBUS_IFACE_AUTH_NAME)
4720+ iface.login('http://localhost', self.u1login.OAUTH_CONSUMER,
4721+ reply_handler=login_handler,
4722+ error_handler=self.error_handler)
4723+ return d
4724+
4725+ def test_auth_denied(self):
4726+ """ Test that denying authorization works correctly. """
4727+
4728+ def new_creds(realm=None, consumer_key=None, sender=None):
4729+ """ Override the callback """
4730+ d.errback(Failure(InvalidSignalError()))
4731+
4732+ def auth_denied():
4733+ """ Override the callback """
4734+ d.callback(True)
4735+
4736+ def got_oauth_error(message=None):
4737+ """ override the callback """
4738+ d.errback(Failure(InvalidSignalError()))
4739+
4740+ login = self.u1login.LoginMain()
4741+ login.main = self.main
4742+ login.new_credentials = new_creds
4743+ login.auth_denied = auth_denied
4744+ login.got_oauth_error = got_oauth_error
4745+
4746+ login._connect_dbus_signals()
4747+
4748+ self.oauth.processor.next_login_with(self.oauth.processor.got_denial)
4749+
4750+ client = self.bus.get_object(self.u1login.DBUS_IFACE_AUTH_NAME,
4751+ '/oauthdesktop',
4752+ follow_name_owner_changes=True)
4753+ d = defer.Deferred()
4754+
4755+ def login_handler():
4756+ """ login handler """
4757+ return
4758+
4759+ iface = dbus.Interface(client, self.u1login.DBUS_IFACE_AUTH_NAME)
4760+ iface.login('http://localhost', self.u1login.OAUTH_CONSUMER,
4761+ reply_handler=login_handler,
4762+ error_handler=self.error_handler)
4763+ return d
4764+
4765+ def test_oauth_error(self):
4766+ """ Test that getting an error works correctly. """
4767+
4768+ def new_creds(realm=None, consumer_key=None, sender=None):
4769+ """ Override the callback """
4770+ d.errback(Failure(InvalidSignalError()))
4771+
4772+ def auth_denied():
4773+ """ Override the callback """
4774+ d.errback(Failure(InvalidSignalError()))
4775+
4776+ def got_oauth_error(message=None):
4777+ """ override the callback """
4778+ d.callback(True)
4779+
4780+ login = self.u1login.LoginMain()
4781+ login.main = self.main
4782+ login.new_credentials = new_creds
4783+ login.auth_denied = auth_denied
4784+ login.got_oauth_error = got_oauth_error
4785+
4786+ login._connect_dbus_signals()
4787+
4788+ self.oauth.processor.next_login_with(self.oauth.processor.got_error,
4789+ ('error!',))
4790+
4791+ client = self.bus.get_object(self.u1login.DBUS_IFACE_AUTH_NAME,
4792+ '/oauthdesktop',
4793+ follow_name_owner_changes=True)
4794+ d = defer.Deferred()
4795+
4796+ def login_handler():
4797+ """ login handler """
4798+ return
4799+
4800+ iface = dbus.Interface(client, self.u1login.DBUS_IFACE_AUTH_NAME)
4801+ iface.login('http://localhost', self.u1login.OAUTH_CONSUMER,
4802+ reply_handler=login_handler,
4803+ error_handler=self.error_handler)
4804+ return d
4805
4806=== modified file 'ubuntu_sso/tests/test_main.py'
4807--- ubuntu_sso/tests/test_main.py 2010-06-16 15:11:04 +0000
4808+++ ubuntu_sso/tests/test_main.py 2010-08-11 18:19:01 +0000
4809@@ -1,8 +1,10 @@
4810 # test_main - tests for ubuntu_sso.main
4811 #
4812 # Author: Stuart Langridge <stuart.langridge@canonical.com>
4813+# Author: Natalia Bidart <natalia.bidart@canonical.com>
4814+# Author: Alejandro J. Cura <alecu@canonical.com>
4815 #
4816-# Copyright 2009 Canonical Ltd.
4817+# Copyright 2009-2010 Canonical Ltd.
4818 #
4819 # This program is free software: you can redistribute it and/or modify it
4820 # under the terms of the GNU General Public License version 3, as published
4821@@ -15,29 +17,317 @@
4822 #
4823 # You should have received a copy of the GNU General Public License along
4824 # with this program. If not, see <http://www.gnu.org/licenses/>.
4825-"""Tests for the OAuth client code for StorageFS."""
4826-
4827-import os, StringIO
4828-from mocker import MockerTestCase
4829-from ubuntu_sso import config
4830-from ubuntu_sso.main import LoginProcessor, BadRealmError
4831+"""Tests for the OAuth client code."""
4832+
4833+import logging
4834+import os
4835+import StringIO
4836+
4837+import gobject
4838+
4839+from lazr.restfulclient.errors import HTTPError
4840+from mocker import Mocker, MockerTestCase, ARGS, KWARGS
4841+from twisted.internet.defer import Deferred
4842+from twisted.trial.unittest import TestCase
4843+
4844+import ubuntu_sso.main
4845+
4846+from contrib.testing.testcase import MementoHandler
4847+from ubuntu_sso import config, gui
4848+from ubuntu_sso.main import (
4849+ AuthenticationError, BadRealmError, blocking, EmailTokenError,
4850+ InvalidEmailError, InvalidPasswordError, SSOLogin, SSOCredentials,
4851+ get_token_name, keyring_get_credentials, keyring_store_credentials, logger,
4852+ LoginProcessor, NewPasswordError, SSOLoginProcessor, RegistrationError,
4853+ ResetPasswordTokenError)
4854+
4855+
4856+APP_NAME = 'The Coolest App Ever'
4857+CAPTCHA_PATH = os.path.abspath(os.path.join(os.curdir, 'ubuntu_sso', 'tests',
4858+ 'files', 'captcha.png'))
4859+CAPTCHA_ID = 'test'
4860+CAPTCHA_SOLUTION = 'william Byrd'
4861+CANT_RESET_PASSWORD_CONTENT = "CanNotResetPassowrdError: " \
4862+ "Can't reset password for this account"
4863+RESET_TOKEN_INVALID_CONTENT = "AuthToken matching query does not exist."
4864+EMAIL = 'test@example.com'
4865+EMAIL_TOKEN = 'B2Pgtf'
4866+HELP = 'help text'
4867+PASSWORD = 'be4tiFul'
4868+RESET_PASSWORD_TOKEN = '8G5Wtq'
4869+TOKEN = {u'consumer_key': u'xQ7xDAz',
4870+ u'consumer_secret': u'KzCJWCTNbbntwfyCKKjomJDzlgqxLy',
4871+ u'name': u'test',
4872+ u'token': u'GkInOfSMGwTXAUoVQwLUoPxElEEUdhsLVNTPhxHJDUIeHCPNEo',
4873+ u'token_secret': u'qFYImEtlczPbsCnYyuwLoPDlPEnvNcIktZphPQklAWrvyfFMV'}
4874+STATUS_UNKNOWN = {'status': 'yadda-yadda'}
4875+STATUS_ERROR = {'status': 'error', 'errors': {'something': ['Bla', 'Ble']}}
4876+STATUS_OK = {'status': 'ok'}
4877+STATUS_EMAIL_UNKNOWN = {'status': 'yadda-yadda'}
4878+STATUS_EMAIL_ERROR = {'errors': {'email_token': ['Error1', 'Error2']}}
4879+STATUS_EMAIL_OK = {'email': 'okmail@okserver.okdomain'}
4880+TC_URL = 'tcurl'
4881+WINDOW_ID = 5
4882+
4883+DEFAULT_ARGS = (APP_NAME, TC_URL, HELP, WINDOW_ID)
4884+
4885+
4886+class FakedCaptchas(object):
4887+ """Fake the captcha generator."""
4888+
4889+ def new(self):
4890+ """Return a fix captcha)."""
4891+ return {'image_url': 'file://%s' % CAPTCHA_PATH,
4892+ 'captcha_id': CAPTCHA_ID}
4893+
4894+
4895+class FakedRegistrations(object):
4896+ """Fake the registrations service."""
4897+
4898+ def register(self, email, password, captcha_id, captcha_solution):
4899+ """Fake registration. Return a fix result."""
4900+ if captcha_id is None and captcha_solution is None:
4901+ return STATUS_UNKNOWN
4902+ elif captcha_id != CAPTCHA_ID or captcha_solution != CAPTCHA_SOLUTION:
4903+ return STATUS_ERROR
4904+ else:
4905+ return STATUS_OK
4906+
4907+ def request_password_reset_token(self, email):
4908+ """Fake password reset token. Return a fix result."""
4909+ if email is None:
4910+ return STATUS_UNKNOWN
4911+ elif email != EMAIL:
4912+ raise HTTPError(response=None, content=CANT_RESET_PASSWORD_CONTENT)
4913+ else:
4914+ return STATUS_OK
4915+
4916+ def set_new_password(self, email, token, new_password):
4917+ """Fake the setting of new password. Return a fix result."""
4918+ if email is None and token is None and new_password is None:
4919+ return STATUS_UNKNOWN
4920+ elif email != EMAIL or token != RESET_PASSWORD_TOKEN:
4921+ raise HTTPError(response=None, content=RESET_TOKEN_INVALID_CONTENT)
4922+ else:
4923+ return STATUS_OK
4924+
4925+
4926+class FakedAuthentications(object):
4927+ """Fake the authentications service."""
4928+
4929+ def authenticate(self, token_name):
4930+ """Fake authenticate. Return a fix result."""
4931+ if not token_name.startswith(get_token_name(APP_NAME)):
4932+ raise HTTPError(response=None, content=None)
4933+ else:
4934+ return TOKEN
4935+
4936+
4937+class FakedAccounts(object):
4938+ """Fake the accounts service."""
4939+
4940+ def validate_email(self, email_token):
4941+ """Fake the email validation. Return a fix result."""
4942+ if email_token is None:
4943+ return STATUS_EMAIL_UNKNOWN
4944+ if email_token != EMAIL_TOKEN:
4945+ return STATUS_EMAIL_ERROR
4946+ else:
4947+ return STATUS_EMAIL_OK
4948+
4949+
4950+class FakedSSOServer(object):
4951+ """Fake an SSO server."""
4952+
4953+ def __init__(self, authorizer, service_root):
4954+ self.captchas = FakedCaptchas()
4955+ self.registrations = FakedRegistrations()
4956+ self.authentications = FakedAuthentications()
4957+ self.accounts = FakedAccounts()
4958+
4959+
4960+class SSOLoginProcessorTestCase(TestCase, MockerTestCase):
4961+ """Test suite for the SSO login processor."""
4962+
4963+ def setUp(self):
4964+ """Init."""
4965+ self.processor = SSOLoginProcessor(sso_service_class=FakedSSOServer)
4966+ self.register_kwargs = dict(email=EMAIL, password=PASSWORD,
4967+ captcha_id=CAPTCHA_ID,
4968+ captcha_solution=CAPTCHA_SOLUTION)
4969+ self.login_kwargs = dict(email=EMAIL, password=PASSWORD,
4970+ app_name=APP_NAME)
4971+
4972+ def ksc(k, v):
4973+ self.assertEqual(k, APP_NAME)
4974+ self.assertEqual(v, TOKEN)
4975+
4976+ self.patch(ubuntu_sso.main, "keyring_store_credentials", ksc)
4977+
4978+ def tearDown(self):
4979+ """Clean up."""
4980+ self.processor = None
4981+
4982+ def test_generate_captcha(self):
4983+ """Captcha can be generated."""
4984+ filename = self.mktemp()
4985+ self.addCleanup(lambda: os.remove(filename))
4986+ captcha_id = self.processor.generate_captcha(filename)
4987+ self.assertEqual(CAPTCHA_ID, captcha_id, 'captcha id must be correct.')
4988+ self.assertTrue(os.path.isfile(filename), '%s must exist.' % filename)
4989+
4990+ with open(CAPTCHA_PATH) as f:
4991+ expected = f.read()
4992+ with open(filename) as f:
4993+ actual = f.read()
4994+ self.assertEqual(expected, actual, 'captcha image must be correct.')
4995+
4996+ def test_register_user_checks_valid_email(self):
4997+ """Email is validated."""
4998+ self.register_kwargs['email'] = 'notavalidemail'
4999+ self.assertRaises(InvalidEmailError,
5000+ self.processor.register_user, **self.register_kwargs)
The diff has been truncated for viewing.

Subscribers

People subscribed via source and target branches

to all changes: