Merge lp:~nataliabidart/ubuntu/natty/ubuntu-sso-client/ubuntu-sso-client-1.1.1 into lp:ubuntu/natty/ubuntu-sso-client

Proposed by Natalia Bidart on 2010-10-18
Status: Merged
Merged at revision: 18
Proposed branch: lp:~nataliabidart/ubuntu/natty/ubuntu-sso-client/ubuntu-sso-client-1.1.1
Merge into: lp:ubuntu/natty/ubuntu-sso-client
Diff against target: 5129 lines (+2562/-1536)
27 files modified
PKG-INFO (+1/-1)
bin/ubuntu-sso-login (+18/-24)
contrib/testing/testcase.py (+25/-0)
data/ui.glade (+46/-44)
debian/changelog (+77/-0)
debian/watch (+1/-1)
po/ubuntu-sso-client.pot (+178/-0)
pylintrc (+1/-4)
run-tests (+10/-7)
setup.py (+1/-1)
ubuntu_sso/__init__.py (+9/-3)
ubuntu_sso/account.py (+231/-0)
ubuntu_sso/auth.py (+0/-25)
ubuntu_sso/credentials.py (+271/-0)
ubuntu_sso/gui.py (+97/-53)
ubuntu_sso/keyring.py (+2/-0)
ubuntu_sso/logger.py (+12/-11)
ubuntu_sso/main.py (+188/-347)
ubuntu_sso/networkstate.py (+2/-0)
ubuntu_sso/tests/__init__.py (+31/-1)
ubuntu_sso/tests/bin/show_gui (+4/-4)
ubuntu_sso/tests/test_account.py (+335/-0)
ubuntu_sso/tests/test_credentials.py (+444/-0)
ubuntu_sso/tests/test_gui.py (+205/-139)
ubuntu_sso/tests/test_keyring.py (+3/-2)
ubuntu_sso/tests/test_main.py (+368/-869)
ubuntu_sso/tests/test_networkstate.py (+2/-0)
To merge this branch: bzr merge lp:~nataliabidart/ubuntu/natty/ubuntu-sso-client/ubuntu-sso-client-1.1.1
Reviewer Review Type Date Requested Status
Ubuntu branches 2010-10-18 Pending
Review via email: mp+38716@code.launchpad.net

Description of the Change

  * Adding .bzr-builddeb/default.conf as per Michael Vog (mvo) request.

  * Adding dpkg (>= 1.15.7.2) as Pre-Depends (fixes LP: #658768).

  * Adding gnome-keyring as dep since python-gnomekeyring doesn't install it.

  * New upstream release (1.1.1):

    [ Alejandro J. Cura <email address hidden> ]
      * Call the dbus mainloop thread init (fixes LP: #656545).
    [ Natalia B. Bidart <email address hidden> ]
      * Added the new dbus iface to the executable. Deprecated a few Dbus
      constants.
    [ Natalia B. Bidart <email address hidden> ]
      * Adding a new DBus iface to manage credentials with flexible API (LP:
      #653113).
    [ Natalia B. Bidart <email address hidden> ]
      * Replace twisted gtk reactor with the standard gtk mainloop. (LP:
      #655327).
    [ Natalia B. Bidart <email address hidden> ]
      * The TC browser will not be shown until the TOS page has been loaded. A
      spinner is shown in the mean time (LP: #620456).
      * The TC browser does not allow link navigation other than the TOS page
      (LP: #616961).
    [ Natalia B. Bidart <email address hidden> ]
      * Terms and conditions label is now translatable. Added a button to be
      able to browse the TC (LP: 654534).
    [ Natalia B. Bidart <email address hidden> ]
      * Moved logic from DBus SSOCredentials module into a self-contained, DBus
      agnostic module called 'credentials' (LP: #653113).
    [ Natalia B. Bidart <email address hidden> ]
      * Isolating SSO login processor into a separated module (LP: #637204).
    [ Natalia B. Bidart <email address hidden> ]
      * Cleanup code and modules to prepare the code for the DBus API refactor.
    [ Natalia B. Bidart <email address hidden> ]
      * Made entries name to be unicode to avoid a faulty capitalization when
      using turkish locale (LP: #652939).
    [ Natalia B. Bidart <email address hidden> ]
      * Workaround for LP: #467397 (when using turkish locale, decimal module
      import fails).
    [ Natalia B. Bidart <email address hidden> ]
      * Added encoding declaration on every file. Fixed error passing from GUI
      in GTK signals (LP: #652318).
    [ Natalia B. Bidart <email address hidden> ]
      * Avoiding calling a callback twice for buttons (LP: #652248).
    [ Natalia B. Bidart <email address hidden> ]
      * Removed unneeded assert sentences and replaced with warning logging
      when necessary (LP: #649317).

  * New upstream release (1.1.0):
    [ Natalia B. Bidart <email address hidden> ]
      * Avoiding having the main window freezing when pinging the U1 server
      (LP: #645162).
    [ Natalia B. Bidart <email address hidden> ]
      * Do not create the login keyring, use default instead (LP: #639690).
    [ Natalia B. Bidart <email address hidden> ]
      * Avoid duplicating logging messages for module "main" (LP: #638981).
    [ Natalia B. Bidart <email address hidden> ]
      * Delay import of webkit to be able to build on non-graphical envs (LP:
      #628956).

To post a comment you must log in.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'PKG-INFO'
2--- PKG-INFO 2010-10-01 14:57:22 +0000
3+++ PKG-INFO 2010-10-18 13:21:21 +0000
4@@ -1,6 +1,6 @@
5 Metadata-Version: 1.1
6 Name: ubuntu-sso-client
7-Version: 1.0.3
8+Version: 1.1.1
9 Summary: Ubuntu Single Sign-On client
10 Home-page: https://launchpad.net/ubuntu-sso-client
11 Author: Natalia Bidart
12
13=== modified file 'bin/ubuntu-sso-login'
14--- bin/ubuntu-sso-login 2010-10-01 14:57:22 +0000
15+++ bin/ubuntu-sso-login 2010-10-18 13:21:21 +0000
16@@ -42,13 +42,15 @@
17 import signal
18 import sys
19
20+import dbus.mainloop.glib
21 import dbus.service
22 import gtk
23
24 from dbus.mainloop.glib import DBusGMainLoop
25
26-from ubuntu_sso import DBUS_BUS_NAME, DBUS_CRED_PATH
27-from ubuntu_sso.main import SSOLogin, SSOCredentials
28+from ubuntu_sso import (DBUS_BUS_NAME, DBUS_ACCOUNT_PATH, DBUS_CRED_PATH,
29+ DBUS_CREDENTIALS_PATH)
30+from ubuntu_sso.main import SSOLogin, SSOCredentials, CredentialsManagement
31
32 from ubuntu_sso.logger import setup_logging
33
34@@ -57,21 +59,21 @@
35
36
37 logger = setup_logging("ubuntu-sso-login")
38+dbus.mainloop.glib.threads_init()
39 gtk.gdk.threads_init()
40 DBusGMainLoop(set_as_default=True)
41
42
43 def sighup_handler(*a, **kw):
44- """Stop the service.
45-
46- This is not thread safe, see the link below for info:
47- www.listware.net/201004/gtk-devel-list/115067-unix-signals-in-glib.html
48- """
49- from twisted.internet import reactor
50- # Module 'twisted.internet.reactor' has no 'stop' member
51- # pylint: disable=E1101
52+ """Stop the service."""
53+ # This handler may be called in any thread, so is not thread safe.
54+ # See the link below for info:
55+ # www.listware.net/201004/gtk-devel-list/115067-unix-signals-in-glib.html
56+ #
57+ # gtk.main_quit and the logger methods are safe to be called from any thread.
58+ # Just don't call other random stuff here.
59 logger.info("Stoping Ubuntu SSO login manager since SIGHUP was received.")
60- reactor.stop()
61+ gtk.main_quit()
62
63
64 if __name__ == "__main__":
65@@ -83,21 +85,13 @@
66 logger.error("Ubuntu SSO login manager already running, quitting.")
67 sys.exit(0)
68
69- logger.debug("Installing the Twisted gtk2reactor.")
70- from twisted.internet import gtk2reactor
71- gtk2reactor.install()
72-
73 logger.debug("Hooking up SIGHUP with handler %r.", sighup_handler)
74 signal.signal(signal.SIGHUP, sighup_handler)
75
76 logger.info("Starting Ubuntu SSO login manager for bus %r.", DBUS_BUS_NAME)
77- SSOLogin(dbus.service.BusName(DBUS_BUS_NAME,
78- bus=dbus.SessionBus()))
79- SSOCredentials(dbus.service.BusName(DBUS_BUS_NAME,
80- bus=dbus.SessionBus()),
81- object_path=DBUS_CRED_PATH)
82+ bus_name = dbus.service.BusName(DBUS_BUS_NAME, bus=dbus.SessionBus())
83+ SSOLogin(bus_name, object_path=DBUS_ACCOUNT_PATH)
84+ SSOCredentials(bus_name, object_path=DBUS_CRED_PATH)
85+ CredentialsManagement(bus_name, object_path=DBUS_CREDENTIALS_PATH)
86
87- from twisted.internet import reactor
88- # Module 'twisted.internet.reactor' has no 'run' member
89- # pylint: disable=E1101
90- reactor.run()
91+ gtk.main()
92
93=== modified file 'contrib/testing/testcase.py'
94--- contrib/testing/testcase.py 2010-09-08 19:25:02 +0000
95+++ contrib/testing/testcase.py 2010-10-18 13:21:21 +0000
96@@ -96,3 +96,28 @@
97 if rec.levelno == level and all(m in rec.message for m in msgs):
98 return True
99 return False
100+
101+ def check_debug(self, *msgs):
102+ """Shortcut for checking in DEBUG."""
103+ return self.check(logging.DEBUG, *msgs)
104+
105+ def check_info(self, *msgs):
106+ """Shortcut for checking in INFO."""
107+ return self.check(logging.INFO, *msgs)
108+
109+ def check_warning(self, *msgs):
110+ """Shortcut for checking in WARNING."""
111+ return self.check(logging.WARNING, *msgs)
112+
113+ def check_error(self, *msgs):
114+ """Shortcut for checking in ERROR."""
115+ return self.check(logging.ERROR, *msgs)
116+
117+ def check_exception(self, exception_class, *msgs):
118+ """Shortcut for checking exceptions."""
119+ for rec in self.records:
120+ if rec.levelno == logging.ERROR and \
121+ all(m in rec.exc_text for m in msgs) and \
122+ exception_class == rec.exc_info[0]:
123+ return True
124+ return False
125
126=== modified file 'data/ui.glade'
127--- data/ui.glade 2010-08-27 22:16:40 +0000
128+++ data/ui.glade 2010-10-18 13:21:21 +0000
129@@ -1,4 +1,4 @@
130-<?xml version="1.0"?>
131+<?xml version="1.0" encoding="UTF-8"?>
132 <interface>
133 <requires lib="gtk+" version="2.16"/>
134 <!-- interface-naming-policy project-wide -->
135@@ -60,12 +60,6 @@
136 <property name="visible">True</property>
137 <property name="spacing">10</property>
138 <child>
139- <placeholder/>
140- </child>
141- <child>
142- <placeholder/>
143- </child>
144- <child>
145 <object class="GtkHBox" id="emails_hbox">
146 <property name="visible">True</property>
147 <property name="spacing">5</property>
148@@ -79,7 +73,7 @@
149 </object>
150 <packing>
151 <property name="expand">False</property>
152- <property name="position">2</property>
153+ <property name="position">0</property>
154 </packing>
155 </child>
156 <child>
157@@ -96,17 +90,18 @@
158 </object>
159 <packing>
160 <property name="expand">False</property>
161- <property name="position">4</property>
162+ <property name="position">1</property>
163 </packing>
164 </child>
165 <child>
166 <object class="GtkLabel" id="password_help_label">
167 <property name="visible">True</property>
168+ <property name="label">password help</property>
169 <property name="wrap">True</property>
170 </object>
171 <packing>
172 <property name="expand">False</property>
173- <property name="position">5</property>
174+ <property name="position">2</property>
175 </packing>
176 </child>
177 <child>
178@@ -114,6 +109,8 @@
179 <property name="visible">True</property>
180 <child>
181 <object class="GtkVBox" id="captcha_vbox">
182+ <property name="width_request">300</property>
183+ <property name="height_request">60</property>
184 <property name="visible">True</property>
185 <property name="homogeneous">True</property>
186 <child>
187@@ -184,7 +181,7 @@
188 </child>
189 </object>
190 <packing>
191- <property name="position">7</property>
192+ <property name="position">3</property>
193 </packing>
194 </child>
195 <child>
196@@ -194,12 +191,9 @@
197 <child>
198 <placeholder/>
199 </child>
200- <child>
201- <placeholder/>
202- </child>
203 </object>
204 <packing>
205- <property name="position">8</property>
206+ <property name="position">4</property>
207 </packing>
208 </child>
209 <child>
210@@ -213,12 +207,13 @@
211 </object>
212 <packing>
213 <property name="expand">False</property>
214- <property name="position">9</property>
215+ <property name="position">5</property>
216 </packing>
217 </child>
218 <child>
219- <object class="GtkHBox" id="tc_hbox">
220+ <object class="GtkVBox" id="tc_vbox">
221 <property name="visible">True</property>
222+ <property name="spacing">5</property>
223 <child>
224 <object class="GtkCheckButton" id="yes_to_tc_checkbutton">
225 <property name="label" translatable="yes">yes to tc</property>
226@@ -234,33 +229,41 @@
227 </packing>
228 </child>
229 <child>
230- <object class="GtkLinkButton" id="tc_button">
231- <property name="label" translatable="yes">tc button</property>
232+ <object class="GtkHButtonBox" id="hbuttonbox3">
233 <property name="visible">True</property>
234- <property name="can_focus">True</property>
235- <property name="receives_default">True</property>
236- <property name="has_tooltip">True</property>
237- <property name="relief">none</property>
238- <signal name="clicked" handler="on_tc_button_clicked"/>
239+ <property name="layout_style">start</property>
240+ <child>
241+ <object class="GtkButton" id="tc_button">
242+ <property name="label">show tc</property>
243+ <property name="visible">True</property>
244+ <property name="can_focus">True</property>
245+ <property name="receives_default">True</property>
246+ <signal name="clicked" handler="on_tc_button_clicked"/>
247+ </object>
248+ <packing>
249+ <property name="expand">False</property>
250+ <property name="fill">False</property>
251+ <property name="position">1</property>
252+ </packing>
253+ </child>
254 </object>
255 <packing>
256- <property name="expand">False</property>
257 <property name="position">1</property>
258 </packing>
259 </child>
260- </object>
261- <packing>
262- <property name="expand">False</property>
263- <property name="position">10</property>
264- </packing>
265- </child>
266- <child>
267- <object class="GtkLabel" id="tc_warning_label">
268- <property name="visible">True</property>
269- <property name="wrap">True</property>
270- </object>
271- <packing>
272- <property name="position">11</property>
273+ <child>
274+ <object class="GtkLabel" id="tc_warning_label">
275+ <property name="visible">True</property>
276+ <property name="label">tc warning</property>
277+ <property name="wrap">True</property>
278+ </object>
279+ <packing>
280+ <property name="position">2</property>
281+ </packing>
282+ </child>
283+ </object>
284+ <packing>
285+ <property name="position">6</property>
286 </packing>
287 </child>
288 <child>
289@@ -300,7 +303,7 @@
290 </object>
291 <packing>
292 <property name="expand">False</property>
293- <property name="position">12</property>
294+ <property name="position">7</property>
295 </packing>
296 </child>
297 <child>
298@@ -315,12 +318,9 @@
299 </object>
300 <packing>
301 <property name="expand">False</property>
302- <property name="position">13</property>
303+ <property name="position">8</property>
304 </packing>
305 </child>
306- <child>
307- <placeholder/>
308- </child>
309 </object>
310 <object class="GtkVBox" id="processing_vbox">
311 <property name="visible">True</property>
312@@ -365,7 +365,6 @@
313 <object class="GtkVBox" id="tc_browser_vbox">
314 <property name="visible">True</property>
315 <signal name="hide" handler="on_tc_browser_vbox_hide"/>
316- <signal name="show" handler="on_tc_browser_vbox_show"/>
317 <child>
318 <object class="GtkScrolledWindow" id="tc_browser_window">
319 <property name="visible">True</property>
320@@ -618,6 +617,9 @@
321 <property name="label" translatable="yes">label</property>
322 <property name="wrap">True</property>
323 </object>
324+ <packing>
325+ <property name="position">1</property>
326+ </packing>
327 </child>
328 </object>
329 <packing>
330
331=== modified file 'debian/changelog'
332--- debian/changelog 2010-10-01 15:35:43 +0000
333+++ debian/changelog 2010-10-18 13:21:21 +0000
334@@ -1,3 +1,80 @@
335+ubuntu-sso-client (1.1.1-0ubuntu1) UNRELEASED; urgency=low
336+
337+ * New upstream release (1.1.1):
338+
339+ [ Alejandro J. Cura <alecu@canonical.com> ]
340+ * Call the dbus mainloop thread init (fixes LP: #656545).
341+
342+ [ Natalia B. Bidart <natalia.bidart@canonical.com> ]
343+ * Added the new dbus iface to the executable. Deprecated a few Dbus
344+ constants.
345+
346+ [ Natalia B. Bidart <natalia.bidart@canonical.com> ]
347+ * Adding a new DBus iface to manage credentials with flexible API (LP:
348+ #653113).
349+
350+ [ Natalia B. Bidart <natalia.bidart@canonical.com> ]
351+ * Replace twisted gtk reactor with the standard gtk mainloop. (LP:
352+ #655327).
353+
354+ [ Natalia B. Bidart <natalia.bidart@canonical.com> ]
355+ * The TC browser will not be shown until the TOS page has been loaded. A
356+ spinner is shown in the mean time (LP: #620456).
357+ * The TC browser does not allow link navigation other than the TOS page
358+ (LP: #616961).
359+
360+ [ Natalia B. Bidart <natalia.bidart@canonical.com> ]
361+ * Terms and conditions label is now translatable. Added a button to be
362+ able to browse the TC (LP: 654534).
363+
364+ [ Natalia B. Bidart <natalia.bidart@canonical.com> ]
365+ * Moved logic from DBus SSOCredentials module into a self-contained, DBus
366+ agnostic module called 'credentials' (LP: #653113).
367+
368+ [ Natalia B. Bidart <natalia.bidart@canonical.com> ]
369+ * Isolating SSO login processor into a separated module (LP: #637204).
370+
371+ [ Natalia B. Bidart <natalia.bidart@canonical.com> ]
372+ * Cleanup code and modules to prepare the code for the DBus API refactor.
373+
374+ [ Natalia B. Bidart <natalia.bidart@canonical.com> ]
375+ * Made entries name to be unicode to avoid a faulty capitalization when
376+ using turkish locale (LP: #652939).
377+
378+ [ Natalia B. Bidart <natalia.bidart@canonical.com> ]
379+ * Workaround for LP: #467397 (when using turkish locale, decimal module
380+ import fails).
381+
382+ [ Natalia B. Bidart <natalia.bidart@canonical.com> ]
383+ * Added encoding declaration on every file. Fixed error passing from GUI
384+ in GTK signals (LP: #652318).
385+
386+ [ Natalia B. Bidart <natalia.bidart@canonical.com> ]
387+ * Avoiding calling a callback twice for buttons (LP: #652248).
388+
389+ [ Natalia B. Bidart <natalia.bidart@canonical.com> ]
390+ * Removed unneeded assert sentences and replaced with warning logging
391+ when necessary (LP: #649317).
392+
393+ * New upstream release (1.1.0):
394+
395+ [ Natalia B. Bidart <natalia.bidart@canonical.com> ]
396+ * Avoiding having the main window freezing when pinging the U1 server
397+ (LP: #645162).
398+
399+ [ Natalia B. Bidart <natalia.bidart@canonical.com> ]
400+ * Do not create the login keyring, use default instead (LP: #639690).
401+
402+ [ Natalia B. Bidart <natalia.bidart@canonical.com> ]
403+ * Avoid duplicating logging messages for module "main" (LP: #638981).
404+
405+ [ Natalia B. Bidart <natalia.bidart@canonical.com> ]
406+ * Delay import of webkit to be able to build on non-graphical envs (LP:
407+ #628956).
408+
409+
410+ -- Natalia Bidart (nessita) <nataliabidart@gmail.com> Mon, 11 Oct 2010 10:22:28 -0300
411+
412 ubuntu-sso-client (1.0.3-0ubuntu1) maverick; urgency=low
413
414 * New upstream release:
415
416=== modified file 'debian/watch'
417--- debian/watch 2010-09-22 18:53:18 +0000
418+++ debian/watch 2010-10-18 13:21:21 +0000
419@@ -1,3 +1,3 @@
420 version=3
421-http://launchpad.net/ubuntu-sso-client/+download?start=10 .*/ubuntu-sso-client-([0-9.]+)\.tar\.gz
422+http://launchpad.net/ubuntu-sso-client/+download .*/ubuntu-sso-client-([0-9.]+)\.tar\.gz
423
424
425=== added file 'po/ubuntu-sso-client.pot'
426--- po/ubuntu-sso-client.pot 1970-01-01 00:00:00 +0000
427+++ po/ubuntu-sso-client.pot 2010-10-18 13:21:21 +0000
428@@ -0,0 +1,178 @@
429+# SOME DESCRIPTIVE TITLE.
430+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
431+# This file is distributed under the same license as the PACKAGE package.
432+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
433+#
434+#, fuzzy
435+msgid ""
436+msgstr ""
437+"Project-Id-Version: PACKAGE VERSION\n"
438+"Report-Msgid-Bugs-To: \n"
439+"POT-Creation-Date: 2010-10-11 09:34-0300\n"
440+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
441+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
442+"Language-Team: LANGUAGE <LL@li.org>\n"
443+"Language: \n"
444+"MIME-Version: 1.0\n"
445+"Content-Type: text/plain; charset=CHARSET\n"
446+"Content-Transfer-Encoding: 8bit\n"
447+
448+#: ../ubuntu_sso/gui.py:207
449+msgid "Type the characters above"
450+msgstr ""
451+
452+#: ../ubuntu_sso/gui.py:208
453+#, python-format
454+msgid "To connect this computer to %(app_name)s enter your details below."
455+msgstr ""
456+
457+#: ../ubuntu_sso/gui.py:210 ../ubuntu_sso/gui.py:221 ../ubuntu_sso/gui.py:239
458+msgid "Email address"
459+msgstr ""
460+
461+#: ../ubuntu_sso/gui.py:211
462+msgid "Re-type Email address"
463+msgstr ""
464+
465+#: ../ubuntu_sso/gui.py:212
466+msgid ""
467+"The email addresses don't match, please double check and try entering them "
468+"again."
469+msgstr ""
470+
471+#: ../ubuntu_sso/gui.py:214
472+msgid "The email must be a valid email address."
473+msgstr ""
474+
475+#: ../ubuntu_sso/gui.py:215
476+msgid "Enter code verification here"
477+msgstr ""
478+
479+#: ../ubuntu_sso/gui.py:216
480+msgid "This field is required."
481+msgstr ""
482+
483+#: ../ubuntu_sso/gui.py:217
484+msgid "I've forgotten my password"
485+msgstr ""
486+
487+#: ../ubuntu_sso/gui.py:218
488+#, python-format
489+msgid "Create %(app_name)s account"
490+msgstr ""
491+
492+#: ../ubuntu_sso/gui.py:219
493+msgid "Loading..."
494+msgstr ""
495+
496+#: ../ubuntu_sso/gui.py:220
497+msgid "Already have an account? Click here to sign in"
498+msgstr ""
499+
500+#: ../ubuntu_sso/gui.py:222
501+#, python-format
502+msgid "Connect to %(app_name)s"
503+msgstr ""
504+
505+#: ../ubuntu_sso/gui.py:223 ../ubuntu_sso/gui.py:228
506+msgid "Password"
507+msgstr ""
508+
509+#: ../ubuntu_sso/gui.py:224
510+msgid "Name"
511+msgstr ""
512+
513+#: ../ubuntu_sso/gui.py:225
514+msgid "Next"
515+msgstr ""
516+
517+#: ../ubuntu_sso/gui.py:226
518+msgid "One moment please..."
519+msgstr ""
520+
521+#: ../ubuntu_sso/gui.py:227
522+msgid "Your password was successfully changed."
523+msgstr ""
524+
525+#: ../ubuntu_sso/gui.py:229
526+msgid "Re-type Password"
527+msgstr ""
528+
529+#: ../ubuntu_sso/gui.py:230
530+msgid ""
531+"The password must have a minimum of 8 characters and include one uppercase "
532+"character and one number."
533+msgstr ""
534+
535+#: ../ubuntu_sso/gui.py:232
536+msgid ""
537+"The passwords don't match, please double check and try entering them again."
538+msgstr ""
539+
540+#: ../ubuntu_sso/gui.py:234
541+msgid "The password is too weak."
542+msgstr ""
543+
544+#: ../ubuntu_sso/gui.py:235
545+#, python-format
546+msgid "To reset your %(app_name)s password, enter your email address below:"
547+msgstr ""
548+
549+#: ../ubuntu_sso/gui.py:237
550+msgid "Reset password"
551+msgstr ""
552+
553+#: ../ubuntu_sso/gui.py:238
554+msgid "Reset code"
555+msgstr ""
556+
557+#: ../ubuntu_sso/gui.py:240
558+#, python-format
559+msgid ""
560+"A password reset code has been sent to %(email)s.\n"
561+"Please enter the code below along with your new password."
562+msgstr ""
563+
564+#: ../ubuntu_sso/gui.py:243
565+msgid "The process finished successfully. Congratulations!"
566+msgstr ""
567+
568+#: ../ubuntu_sso/gui.py:244
569+msgid "Show Terms & Conditions"
570+msgstr ""
571+
572+#: ../ubuntu_sso/gui.py:245
573+msgid "Agreeing to the Ubuntu One Terms & Conditions is required to subscribe."
574+msgstr ""
575+
576+#: ../ubuntu_sso/gui.py:247
577+msgid ""
578+"There was an error when trying to complete the process. Please check the "
579+"information and try again."
580+msgstr ""
581+
582+#: ../ubuntu_sso/gui.py:249
583+msgid "Enter verification code"
584+msgstr ""
585+
586+#: ../ubuntu_sso/gui.py:250
587+#, python-format
588+msgid ""
589+"Check %(email)s for an email from Ubuntu Single Sign On. This message "
590+"contains a verification code. Enter the code in the field below and click OK "
591+"to complete creating your %(app_name)s account"
592+msgstr ""
593+
594+#: ../ubuntu_sso/gui.py:255
595+#, python-format
596+msgid "I agree with the %(app_name)s terms and conditions"
597+msgstr ""
598+
599+#: ../ubuntu_sso/gui.py:256
600+#, python-format
601+msgid "Yes! Email me %(app_name)s tips and updates."
602+msgstr ""
603+
604+#: ../ubuntu_sso/gui.py:257
605+msgid "Reload"
606+msgstr ""
607
608=== modified file 'pylintrc'
609--- pylintrc 2010-09-08 19:25:02 +0000
610+++ pylintrc 2010-10-18 13:21:21 +0000
611@@ -25,9 +25,6 @@
612 # Pickle collected data for later comparisons.
613 persistent=no
614
615-# Set the cache size for astng objects.
616-cache-size=500
617-
618 # List of plugins (as comma separated values of python modules names) to load,
619 # usually to register additional checkers.
620 load-plugins=
621@@ -50,7 +47,7 @@
622 #disable-cat=
623
624 # Disable the message(s) with the given id(s) or categories
625-# W0142: *Used * or ** magic*
626+# W0142: Used * or ** magic
627 # W0613: Unused argument 'yyy'
628 # C0302: Too many lines in module
629 disable=R,I,W0142,W0613,C0302
630
631=== modified file 'run-tests'
632--- run-tests 2010-09-08 19:25:02 +0000
633+++ run-tests 2010-10-18 13:21:21 +0000
634@@ -15,11 +15,14 @@
635 # You should have received a copy of the GNU General Public License along
636 # with this program. If not, see <http://www.gnu.org/licenses/>.
637
638-`which xvfb-run` ./contrib/test "$@"
639-pylint contrib ubuntu_sso
640-if [ -x `which pep8` ]; then
641- pep8 --repeat bin/ contrib/ ubuntu_sso/
642-else
643- echo "Please install the 'pep8' package."
644-fi
645+style_check() {
646+ pylint contrib ubuntu_sso
647+ if [ -x `which pep8` ]; then
648+ pep8 --repeat bin/ contrib/ ubuntu_sso/
649+ else
650+ echo "Please install the 'pep8' package."
651+ fi
652+}
653+
654+`which xvfb-run` ./contrib/test "$@" && style_check
655 rm -rf _trial_temp/
656
657=== modified file 'setup.py'
658--- setup.py 2010-10-01 14:57:22 +0000
659+++ setup.py 2010-10-18 13:21:21 +0000
660@@ -86,7 +86,7 @@
661
662 DistUtilsExtra.auto.setup(
663 name='ubuntu-sso-client',
664- version='1.0.3',
665+ version='1.1.1',
666 license='GPL v3',
667 author='Natalia Bidart',
668 author_email='natalia.bidart@canonical.com',
669
670=== modified file 'ubuntu_sso/__init__.py'
671--- ubuntu_sso/__init__.py 2010-09-08 19:25:02 +0000
672+++ ubuntu_sso/__init__.py 2010-10-18 13:21:21 +0000
673@@ -16,10 +16,16 @@
674 """Ubuntu Single Sign On client code."""
675
676 # constants
677-DBUS_PATH_AUTH = "/"
678 DBUS_BUS_NAME = "com.ubuntu.sso"
679-DBUS_PATH = "/sso"
680-DBUS_CRED_PATH = "/credentials"
681+DBUS_PATH = "/sso" # deprecated!
682+DBUS_CRED_PATH = "/credentials" # deprecated!
683+DBUS_ACCOUNT_PATH = "/com/ubuntu/sso/accounts"
684+
685 DBUS_IFACE_AUTH_NAME = "com.ubuntu.sso"
686 DBUS_IFACE_USER_NAME = "com.ubuntu.sso.UserManagement"
687 DBUS_IFACE_CRED_NAME = "com.ubuntu.sso.ApplicationCredentials"
688+
689+DBUS_CREDENTIALS_PATH = "/com/ubuntu/sso/credentials"
690+DBUS_CREDENTIALS_IFACE = "com.ubuntu.sso.CredentialsManagement"
691+
692+NO_OP = lambda *args, **kwargs: None
693
694=== added file 'ubuntu_sso/account.py'
695--- ubuntu_sso/account.py 1970-01-01 00:00:00 +0000
696+++ ubuntu_sso/account.py 2010-10-18 13:21:21 +0000
697@@ -0,0 +1,231 @@
698+# -*- coding: utf-8 -*-
699+
700+# Author: Natalia Bidart <natalia.bidart@canonical.com>
701+#
702+# Copyright 2010 Canonical Ltd.
703+#
704+# This program is free software: you can redistribute it and/or modify it
705+# under the terms of the GNU General Public License version 3, as published
706+# by the Free Software Foundation.
707+#
708+# This program is distributed in the hope that it will be useful, but
709+# WITHOUT ANY WARRANTY; without even the implied warranties of
710+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
711+# PURPOSE. See the GNU General Public License for more details.
712+#
713+# You should have received a copy of the GNU General Public License along
714+# with this program. If not, see <http://www.gnu.org/licenses/>.
715+"""Single Sign On account management."""
716+
717+import os
718+import re
719+import urllib2
720+
721+# Unable to import 'lazr.restfulclient.*'
722+# pylint: disable=F0401
723+from lazr.restfulclient.authorize import BasicHttpAuthorizer
724+from lazr.restfulclient.authorize.oauth import OAuthAuthorizer
725+from lazr.restfulclient.errors import HTTPError
726+from lazr.restfulclient.resource import ServiceRoot
727+# pylint: enable=F0401
728+from oauth import oauth
729+
730+from ubuntu_sso.logger import setup_logging
731+
732+
733+logger = setup_logging("ubuntu_sso.account")
734+SERVICE_URL = "https://login.ubuntu.com/api/1.0"
735+
736+
737+class InvalidEmailError(Exception):
738+ """The email is not valid."""
739+
740+
741+class InvalidPasswordError(Exception):
742+ """The password is not valid.
743+
744+ Must provide at least 8 characters, one upper case, one number.
745+ """
746+
747+
748+class RegistrationError(Exception):
749+ """The registration failed."""
750+
751+
752+class AuthenticationError(Exception):
753+ """The authentication failed."""
754+
755+
756+class EmailTokenError(Exception):
757+ """The email token is not valid."""
758+
759+
760+class ResetPasswordTokenError(Exception):
761+ """The token for password reset could not be generated."""
762+
763+
764+class NewPasswordError(Exception):
765+ """The new password could not be set."""
766+
767+
768+class Account(object):
769+ """Login and register users using the Ubuntu Single Sign On service."""
770+
771+ def __init__(self, sso_service_class=None):
772+ """Create a new SSO Account manager."""
773+ if sso_service_class is None:
774+ self.sso_service_class = ServiceRoot
775+ else:
776+ self.sso_service_class = sso_service_class
777+
778+ self.service_url = os.environ.get('USSOC_SERVICE_URL', SERVICE_URL)
779+
780+ logger.info('Created a new SSO access layer for service url %r',
781+ self.service_url)
782+
783+ def _valid_email(self, email):
784+ """Validate the given email."""
785+ return email is not None and '@' in email
786+
787+ def _valid_password(self, password):
788+ """Validate the given password."""
789+ res = (len(password) > 7 and # at least 8 characters
790+ re.search('[A-Z]', password) and # one upper case
791+ re.search('\d+', password)) # one number
792+ return res
793+
794+ def _format_webservice_errors(self, errdict):
795+ """Turn each list of strings in the errdict into a LF separated str."""
796+ result = {}
797+ for key, val in errdict.iteritems():
798+ # workaround until bug #624955 is solved
799+ if isinstance(val, basestring):
800+ result[key] = val
801+ else:
802+ result[key] = "\n".join(val)
803+ return result
804+
805+ def generate_captcha(self, filename):
806+ """Generate a captcha using the SSO service."""
807+ logger.debug('generate_captcha: requesting captcha, filename: %r',
808+ filename)
809+ sso_service = self.sso_service_class(None, self.service_url)
810+ captcha = sso_service.captchas.new()
811+
812+ # download captcha and save to 'filename'
813+ logger.debug('generate_captcha: server answered: %r', captcha)
814+ try:
815+ res = urllib2.urlopen(captcha['image_url'])
816+ with open(filename, 'wb') as f:
817+ f.write(res.read())
818+ except:
819+ msg = 'generate_captcha crashed while downloading the image.'
820+ logger.exception(msg)
821+ raise
822+
823+ return captcha['captcha_id']
824+
825+ def register_user(self, email, password, captcha_id, captcha_solution):
826+ """Register a new user with 'email' and 'password'."""
827+ logger.debug('register_user: email: %r password: <hidden>, '
828+ 'captcha_id: %r, captcha_solution: %r',
829+ email, captcha_id, captcha_solution)
830+ sso_service = self.sso_service_class(None, self.service_url)
831+ if not self._valid_email(email):
832+ logger.error('register_user: InvalidEmailError for email: %r',
833+ email)
834+ raise InvalidEmailError()
835+ if not self._valid_password(password):
836+ logger.error('register_user: InvalidPasswordError')
837+ raise InvalidPasswordError()
838+
839+ result = sso_service.registrations.register(
840+ email=email, password=password, captcha_id=captcha_id,
841+ captcha_solution=captcha_solution)
842+ logger.info('register_user: email: %r result: %r', email, result)
843+
844+ if result['status'] == 'error':
845+ errorsdict = self._format_webservice_errors(result['errors'])
846+ raise RegistrationError(errorsdict)
847+ elif result['status'] != 'ok':
848+ raise RegistrationError('Received unknown status: %s' % result)
849+ else:
850+ return email
851+
852+ def login(self, email, password, token_name):
853+ """Login a user with 'email' and 'password'."""
854+ logger.debug('login: email: %r password: <hidden>, token_name: %r',
855+ email, token_name)
856+ basic = BasicHttpAuthorizer(email, password)
857+ sso_service = self.sso_service_class(basic, self.service_url)
858+ service = sso_service.authentications.authenticate
859+
860+ try:
861+ credentials = service(token_name=token_name)
862+ except HTTPError:
863+ logger.exception('login failed with:')
864+ raise AuthenticationError()
865+
866+ logger.debug('login: authentication successful! consumer_key: %r, ' \
867+ 'token_name: %r', credentials['consumer_key'], token_name)
868+ return credentials
869+
870+ def validate_email(self, email, password, email_token, token_name):
871+ """Validate an email token for user with 'email' and 'password'."""
872+ logger.debug('validate_email: email: %r password: <hidden>, '
873+ 'email_token: %r, token_name: %r.',
874+ email, email_token, token_name)
875+ token = self.login(email=email, password=password,
876+ token_name=token_name)
877+
878+ oauth_token = oauth.OAuthToken(token['token'], token['token_secret'])
879+ authorizer = OAuthAuthorizer(token['consumer_key'],
880+ token['consumer_secret'],
881+ oauth_token)
882+ sso_service = self.sso_service_class(authorizer, self.service_url)
883+ result = sso_service.accounts.validate_email(email_token=email_token)
884+ logger.info('validate_email: email: %r result: %r', email, result)
885+ if 'errors' in result:
886+ errorsdict = self._format_webservice_errors(result['errors'])
887+ raise EmailTokenError(errorsdict)
888+ elif 'email' in result:
889+ return token
890+ else:
891+ raise EmailTokenError('Received invalid reply: %s' % result)
892+
893+ def request_password_reset_token(self, email):
894+ """Request a token to reset the password for the account 'email'."""
895+ sso_service = self.sso_service_class(None, self.service_url)
896+ service = sso_service.registrations.request_password_reset_token
897+ try:
898+ result = service(email=email)
899+ except HTTPError, e:
900+ logger.exception('request_password_reset_token failed with:')
901+ raise ResetPasswordTokenError(e.content.split('\n')[0])
902+
903+ if result['status'] == 'ok':
904+ return email
905+ else:
906+ raise ResetPasswordTokenError('Received invalid reply: %s' %
907+ result)
908+
909+ def set_new_password(self, email, token, new_password):
910+ """Set a new password for the account 'email' to be 'new_password'.
911+
912+ The 'token' has to be the one resulting from a call to
913+ 'request_password_reset_token'.
914+
915+ """
916+ sso_service = self.sso_service_class(None, self.service_url)
917+ service = sso_service.registrations.set_new_password
918+ try:
919+ result = service(email=email, token=token,
920+ new_password=new_password)
921+ except HTTPError, e:
922+ logger.exception('set_new_password failed with:')
923+ raise NewPasswordError(e.content.split('\n')[0])
924+
925+ if result['status'] == 'ok':
926+ return email
927+ else:
928+ raise NewPasswordError('Received invalid reply: %s' % result)
929
930=== removed file 'ubuntu_sso/auth.py'
931--- ubuntu_sso/auth.py 2010-09-08 19:25:02 +0000
932+++ ubuntu_sso/auth.py 1970-01-01 00:00:00 +0000
933@@ -1,25 +0,0 @@
934-# ubuntu_sso.auth - Client authorization module
935-#
936-# Author: Stuart Langridge <stuart.langridge@canonical.com>
937-#
938-# Copyright 2009 Canonical Ltd.
939-#
940-# This program is free software: you can redistribute it and/or modify it
941-# under the terms of the GNU General Public License version 3, as published
942-# by the Free Software Foundation.
943-#
944-# This program is distributed in the hope that it will be useful, but
945-# WITHOUT ANY WARRANTY; without even the implied warranties of
946-# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
947-# PURPOSE. See the GNU General Public License for more details.
948-#
949-# You should have received a copy of the GNU General Public License along
950-# with this program. If not, see <http://www.gnu.org/licenses/>.
951-"""This is an obsolete module.
952-
953-Do not use it, use the DBus API from main instead (SSOCredentials).
954-"""
955-
956-
957-class NoAccessToken(Exception):
958- """No access token available."""
959
960=== added file 'ubuntu_sso/credentials.py'
961--- ubuntu_sso/credentials.py 1970-01-01 00:00:00 +0000
962+++ ubuntu_sso/credentials.py 2010-10-18 13:21:21 +0000
963@@ -0,0 +1,271 @@
964+# -*- coding: utf-8 -*-
965+
966+# Author: Natalia Bidart <natalia.bidart@canonical.com>
967+#
968+# Copyright 2010 Canonical Ltd.
969+#
970+# This program is free software: you can redistribute it and/or modify it
971+# under the terms of the GNU General Public License version 3, as published
972+# by the Free Software Foundation.
973+#
974+# This program is distributed in the hope that it will be useful, but
975+# WITHOUT ANY WARRANTY; without even the implied warranties of
976+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
977+# PURPOSE. See the GNU General Public License for more details.
978+#
979+# You should have received a copy of the GNU General Public License along
980+# with this program. If not, see <http://www.gnu.org/licenses/>.
981+"""Credential management utilities.
982+
983+'Credentials' provides the following fault-tolerant methods:
984+
985+ * find_credentials
986+ * clear_credentials
987+ * register
988+ * login
989+
990+The first two returns immediately (for now).
991+
992+The second two use the 'success_cb', 'error_cb' and 'denial_cb' to signal the
993+caller when the credentials were retrieved successfully, when there was an
994+error or when the user denied the access to the application, respectively.
995+
996+For details, please read the Credentials class documentation.
997+
998+"""
999+
1000+import traceback
1001+import urllib2
1002+
1003+from functools import wraps
1004+
1005+import gobject
1006+
1007+from oauth import oauth
1008+
1009+from ubuntu_sso import keyring, NO_OP
1010+from ubuntu_sso.logger import setup_logging
1011+
1012+
1013+logger = setup_logging('ubuntu_sso.credentials')
1014+
1015+
1016+APP_NAME_KEY = 'app_name'
1017+TC_URL_KEY = 'tc_url'
1018+HELP_TEXT_KEY = 'help_text'
1019+WINDOW_ID_KEY = 'window_id'
1020+PING_URL_KEY = 'ping_url'
1021+SUCCESS_CB_KEY = 'success_cb'
1022+ERROR_CB_KEY = 'error_cb'
1023+DENIAL_CB_KEY = 'denial_cb'
1024+ERROR_KEY = 'error_message'
1025+ERROR_DETAIL_KEY = 'detailed_error'
1026+
1027+
1028+def keyring_store_credentials(app_name, credentials):
1029+ """Store the credentials in the keyring."""
1030+ logger.info('keyring_store_credentials: app_name "%s".', app_name)
1031+ keyring.Keyring(app_name).set_ubuntusso_attr(credentials)
1032+
1033+
1034+def keyring_get_credentials(app_name):
1035+ """Get the credentials from the keyring or None if not there."""
1036+ creds = keyring.Keyring(app_name).get_ubuntusso_attr()
1037+ logger.info('keyring_get_credentials: app_name "%s", resulting credentials'
1038+ ' is not None? %r', app_name, creds is not None)
1039+ return creds
1040+
1041+
1042+def keyring_delete_credentials(app_name):
1043+ """Delete the credentials from the keyring."""
1044+ logger.info('keyring_delete_credentials: app_name "%s".', app_name)
1045+ keyring.Keyring(app_name).delete_ubuntusso_attr()
1046+
1047+
1048+def handle_exceptions(msg):
1049+ """Handle exceptions using 'msg' as error message."""
1050+
1051+ def middle(f):
1052+ """Decorate 'f' to catch all errors."""
1053+
1054+ @wraps(f)
1055+ def inner(self, *a, **kw):
1056+ """Call 'f' within a try-except block.
1057+
1058+ If any exception occurs, self.error_cb is called and the exception
1059+ is logged.
1060+ """
1061+ result = None
1062+ try:
1063+ result = f(self, *a, **kw)
1064+ except: # pylint: disable=W0702
1065+ logger.exception('%s (app_name: %s): %s.',
1066+ f.__name__, self.app_name, msg)
1067+ logger.error('%s (app_name: %s): Calling error_cb at %r.',
1068+ f.__name__, self.app_name, self.error_cb)
1069+ error_dict = {ERROR_KEY: msg,
1070+ ERROR_DETAIL_KEY: traceback.format_exc()}
1071+ self.error_cb(self.app_name, error_dict)
1072+ return result
1073+
1074+ return inner
1075+
1076+ return middle
1077+
1078+
1079+class Credentials(object):
1080+ """Credentials management gateway."""
1081+
1082+ def __init__(self, app_name, tc_url=None, help_text='',
1083+ window_id=0, ping_url=None,
1084+ success_cb=NO_OP, error_cb=NO_OP, denial_cb=NO_OP):
1085+ """Return a Credentials management object.
1086+
1087+ 'app_name' is the application name to be displayed in the GUI.
1088+
1089+ 'tc_url' is the URL pointing to Terms & Conditions. If None, no
1090+ TOS agreement will be displayed.
1091+
1092+ 'help_text' is an explanatory text for the end-users, will be shown
1093+ below the headers.
1094+
1095+ 'window_id' is the id of the window which will be set as a parent of
1096+ the GUI. If 0, no parent will be set.
1097+
1098+ 'ping_url' is the url that will be pinged when a user registers/logins
1099+ successfully. The user email will be attached to 'ping_url'.
1100+
1101+ 'success_cb' will be called when the credentials were retrieved
1102+ successfully. Two params will be passed: the app_name and the
1103+ credentials per se. The credentials is a dictionary of the form:
1104+
1105+ {'token': <value>,
1106+ 'token_secret': <value>,
1107+ 'consumer_key': <value>,
1108+ 'consumer_secret': <value>,
1109+ 'name': <the token name, matches "[app_name] @ [host name]">}
1110+
1111+ 'error_cb' will be called when the credentials retrieval failed. Three
1112+ params will be passed: the app_name, the error message (user friendly,
1113+ not translatable so far), and the detailed error (usually the
1114+ traceback).
1115+
1116+ 'denial_cb' will be called when the user denied the credentials to the
1117+ caller. A single param is passed: the app_name.
1118+
1119+ """
1120+ self.app_name = app_name
1121+ self.help_text = help_text
1122+ self.window_id = window_id
1123+ self.ping_url = ping_url
1124+ self.tc_url = tc_url
1125+ self.success_cb = success_cb
1126+ self.error_cb = error_cb
1127+ self.denial_cb = denial_cb
1128+
1129+ @handle_exceptions(msg='Problem while retrieving credentials')
1130+ def _login_success_cb(self, dialog, app_name, email):
1131+ """Store credentials when the login/registration succeeded.
1132+
1133+ Also, open self.ping_url/email to notify about this new token. If any
1134+ error occur, self.error_cb is called. Otherwise, self.success_cb is
1135+ called.
1136+
1137+ """
1138+ logger.info('Login/registration was successful for app %r, email %r',
1139+ app_name, email)
1140+ creds = self.find_credentials()
1141+ if creds is not None:
1142+ assert len(creds) > 0, 'Creds are empty! This should not happen'
1143+ status = self._ping_url(app_name, email, creds)
1144+ if status is None:
1145+ self.clear_credentials()
1146+ else:
1147+ self.success_cb(app_name, creds)
1148+
1149+ def _login_error_cb(self, dialog, app_name, error):
1150+ """Handle UI error when login/registration failed."""
1151+ logger.warning('Login/registration failed app %r, error %r',
1152+ app_name, error)
1153+ self.error_cb(app_name, {ERROR_KEY: error})
1154+
1155+ def _auth_denial_cb(self, dialog, app_name):
1156+ """The user decided not to allow the registration or login."""
1157+ logger.warning('Login/registration was denied to app %r', app_name)
1158+ self.denial_cb(app_name)
1159+
1160+ @handle_exceptions(msg='Problem opening the ping_url')
1161+ def _ping_url(self, app_name, email, credentials):
1162+ """Ping the self.ping_url with the email attached.
1163+
1164+ Sign the request with the user credentials.
1165+
1166+ """
1167+ logger.info('Maybe pinging server for app_name "%s", ping_url: "%s", '
1168+ 'email "%s".', app_name, self.ping_url, email)
1169+ if self.ping_url is not None:
1170+ url = self.ping_url + email
1171+ consumer = oauth.OAuthConsumer(credentials['consumer_key'],
1172+ credentials['consumer_secret'])
1173+ token = oauth.OAuthToken(credentials['token'],
1174+ credentials['token_secret'])
1175+ get_request = oauth.OAuthRequest.from_consumer_and_token
1176+ oauth_req = get_request(oauth_consumer=consumer, token=token,
1177+ http_method='GET', http_url=url)
1178+ oauth_req.sign_request(oauth.OAuthSignatureMethod_HMAC_SHA1(),
1179+ consumer, token)
1180+ request = urllib2.Request(url, headers=oauth_req.to_header())
1181+ logger.debug('Opening the url "%s" with urllib2.urlopen.',
1182+ request.get_full_url())
1183+ response = urllib2.urlopen(request)
1184+ logger.debug('Url opened. Response: %s.', response.code)
1185+ return response.code
1186+
1187+ @handle_exceptions(msg='Problem opening the Ubuntu SSO user interface')
1188+ def _show_ui(self, login_only):
1189+ """Shows the UI, connect outcome signals."""
1190+ # delay gui import to be able to function on non-graphical envs
1191+ from ubuntu_sso import gui
1192+ app = gui.UbuntuSSOClientGUI(app_name=self.app_name,
1193+ tc_url=self.tc_url, help_text=self.help_text,
1194+ window_id=self.window_id, login_only=login_only)
1195+
1196+ app.connect(gui.SIG_LOGIN_SUCCEEDED, self._login_success_cb)
1197+ app.connect(gui.SIG_LOGIN_FAILED, self._login_error_cb)
1198+ app.connect(gui.SIG_REGISTRATION_SUCCEEDED, self._login_success_cb)
1199+ app.connect(gui.SIG_REGISTRATION_FAILED, self._login_error_cb)
1200+ app.connect(gui.SIG_USER_CANCELATION, self._auth_denial_cb)
1201+
1202+ @handle_exceptions(msg='Problem while retrieving credentials')
1203+ def _login_or_register(self, login_only):
1204+ """Get credentials if found else prompt the GUI."""
1205+ token = self.find_credentials()
1206+ if token is not None and len(token) > 0:
1207+ self.success_cb(self.app_name, token)
1208+ elif token == {}:
1209+ gobject.idle_add(self._show_ui, login_only)
1210+ else:
1211+ # something went wrong with find_credentials, already handled.
1212+ logger.info('_login_or_register: call to "find_credentials" went '
1213+ 'wrong, and was already handled.')
1214+
1215+ @handle_exceptions(msg='Problem while retrieving credentials')
1216+ def find_credentials(self):
1217+ """Get the credentials for 'self.app_name'. Return {} if not there."""
1218+ token = keyring_get_credentials(self.app_name)
1219+ logger.info('find_credentials: self.app_name "%s", '
1220+ 'result is {}? %s', self.app_name, token is None)
1221+ return token if token is not None else {}
1222+
1223+ @handle_exceptions(msg='Problem while deleting credentials')
1224+ def clear_credentials(self):
1225+ """Clear the credentials for 'self.app_name'."""
1226+ keyring_delete_credentials(self.app_name)
1227+
1228+ def register(self):
1229+ """Get credentials if found else prompt the GUI to register."""
1230+ self._login_or_register(login_only=False)
1231+
1232+ def login(self):
1233+ """Get credentials if found else prompt the GUI to login."""
1234+ self._login_or_register(login_only=True)
1235
1236=== modified file 'ubuntu_sso/gui.py'
1237--- ubuntu_sso/gui.py 2010-10-01 14:57:22 +0000
1238+++ ubuntu_sso/gui.py 2010-10-18 13:21:21 +0000
1239@@ -1,3 +1,5 @@
1240+# -*- coding: utf-8 -*-
1241+#
1242 # ubuntu_sso.gui - GUI for login and registration
1243 #
1244 # Author: Natalia Bidart <natalia.bidart@canonical.com>
1245@@ -22,6 +24,7 @@
1246 import os
1247 import re
1248 import tempfile
1249+import webbrowser
1250
1251 from functools import wraps
1252
1253@@ -29,12 +32,11 @@
1254 import gettext
1255 import gobject
1256 import gtk
1257-import webkit
1258 import xdg
1259
1260 from dbus.mainloop.glib import DBusGMainLoop
1261
1262-from ubuntu_sso import DBUS_PATH, DBUS_BUS_NAME, DBUS_IFACE_USER_NAME
1263+from ubuntu_sso import DBUS_ACCOUNT_PATH, DBUS_BUS_NAME, DBUS_IFACE_USER_NAME
1264 from ubuntu_sso.logger import setup_logging
1265
1266
1267@@ -48,6 +50,22 @@
1268 DBusGMainLoop(set_as_default=True)
1269 logger = setup_logging('ubuntu_sso.gui')
1270
1271+# To be removed when Python bindings provide these constants
1272+# as per http://code.google.com/p/pywebkitgtk/issues/detail?id=44
1273+# WebKitLoadStatus
1274+WEBKIT_LOAD_PROVISIONAL = 0
1275+WEBKIT_LOAD_COMMITTED = 1
1276+WEBKIT_LOAD_FINISHED = 2
1277+WEBKIT_LOAD_FIRST_VISUALLY_NON_EMPTY_LAYOUT = 3
1278+WEBKIT_LOAD_FAILED = 4
1279+# WebKitWebNavigationReason
1280+WEBKIT_WEB_NAVIGATION_REASON_LINK_CLICKED = 0
1281+WEBKIT_WEB_NAVIGATION_REASON_FORM_SUBMITTED = 1
1282+WEBKIT_WEB_NAVIGATION_REASON_BACK_FORWARD = 2
1283+WEBKIT_WEB_NAVIGATION_REASON_RELOAD = 3
1284+WEBKIT_WEB_NAVIGATION_REASON_FORM_RESUBMITTED = 4
1285+WEBKIT_WEB_NAVIGATION_REASON_OTHER = 5
1286+
1287
1288 NO_OP = lambda *args, **kwargs: None
1289 DEFAULT_WIDTH = 30
1290@@ -223,7 +241,7 @@
1291 '%(email)s.\nPlease enter the code below ' \
1292 'along with your new password.')
1293 SUCCESS = _('The process finished successfully. Congratulations!')
1294- TC = _('Terms & Conditions')
1295+ TC_BUTTON = _('Show Terms & Conditions')
1296 TC_NOT_ACCEPTED = _('Agreeing to the Ubuntu One Terms & Conditions is ' \
1297 'required to subscribe.')
1298 UNKNOWN_ERROR = _('There was an error when trying to complete the ' \
1299@@ -234,11 +252,11 @@
1300 ' This message contains a verification code.'
1301 ' Enter the code in the field below and click OK'
1302 ' to complete creating your %(app_name)s account'))
1303- YES_TO_TC = _('I agree with the %(app_name)s ') # Terms&Conditions button
1304+ YES_TO_TC = _('I agree with the %(app_name)s terms and conditions')
1305 YES_TO_UPDATES = _('Yes! Email me %(app_name)s tips and updates.')
1306 CAPTCHA_RELOAD_TOOLTIP = _('Reload')
1307
1308- def __init__(self, app_name, tc_uri, help_text,
1309+ def __init__(self, app_name, tc_url, help_text,
1310 window_id=0, login_only=False, close_callback=None):
1311 """Create the GUI and initialize widgets."""
1312 gtk.link_button_set_uri_hook(NO_OP)
1313@@ -249,7 +267,7 @@
1314
1315 self.app_name = app_name
1316 self.app_label = '<b>%s</b>' % self.app_name
1317- self.tc_uri = tc_uri
1318+ self.tc_url = tc_url
1319 self.help_text = help_text
1320 self.close_callback = close_callback
1321
1322@@ -278,8 +296,6 @@
1323 if 'cancel_button' in name:
1324 obj.connect('clicked', self.on_close_clicked)
1325 self.cancels.append(obj)
1326- if '_button' in name:
1327- obj.connect('activate', lambda w: w.clicked())
1328 if 'label' in name:
1329 self.labels.append(obj)
1330
1331@@ -294,14 +310,14 @@
1332 label = getattr(self, name.upper())
1333 is_password = 'password' in name
1334 entry = LabeledEntry(label=label, is_password=is_password)
1335+ entry.set_activates_default(True)
1336 setattr(self, name, entry)
1337- assert getattr(self, name) is not None
1338
1339 self.window.set_icon_name('ubuntu-logo')
1340
1341 self.bus = dbus.SessionBus()
1342 obj = self.bus.get_object(bus_name=DBUS_BUS_NAME,
1343- object_path=DBUS_PATH,
1344+ object_path=DBUS_ACCOUNT_PATH,
1345 follow_name_owner_changes=True)
1346 self.iface_name = DBUS_IFACE_USER_NAME
1347 self.backend = dbus.Interface(object=obj,
1348@@ -417,8 +433,8 @@
1349
1350 match = self.bus.add_signal_receiver(method, signal_name=signal,
1351 dbus_interface=iface)
1352- logger.info('Connecting signal %r with method %r at iface %r.' \
1353- 'Match: %r', signal, method, iface, match)
1354+ logger.debug('Connecting signal %r with method %r at iface %r.' \
1355+ 'Match: %r', signal, method, iface, match)
1356 self._signals_receivers[(iface, signal)] = method
1357
1358 def _debug(self, *args, **kwargs):
1359@@ -537,7 +553,6 @@
1360 self.enter_details_vbox.reorder_child(self.name_entry, 0)
1361 entry = self.captcha_solution_entry
1362 self.captcha_solution_vbox.pack_start(entry, expand=False)
1363- self.captcha_solution_vbox.reorder_child(entry, 0)
1364 msg = self.CAPTCHA_RELOAD_TOOLTIP
1365 self.captcha_reload_button.set_tooltip_text(msg)
1366
1367@@ -556,19 +571,14 @@
1368
1369 msg = self.YES_TO_UPDATES % {'app_name': self.app_name}
1370 self.yes_to_updates_checkbutton.set_label(msg)
1371- if self.tc_uri:
1372+ if self.tc_url:
1373 msg = self.YES_TO_TC % {'app_name': self.app_name}
1374 self.yes_to_tc_checkbutton.set_label(msg)
1375- self.tc_button.set_label(self.TC)
1376+ self.tc_button.set_label(self.TC_BUTTON)
1377 else:
1378- self.tc_hbox.hide_all()
1379+ self.tc_vbox.hide_all()
1380 self.login_button.set_label(self.LOGIN_BUTTON_LABEL)
1381
1382- for entry in (self.name_entry, self.email1_entry, self.email2_entry,
1383- self.password1_entry, self.password2_entry,
1384- self.captcha_solution_entry):
1385- entry.connect('activate', lambda w: self.join_ok_button.clicked())
1386-
1387 return self.enter_details_vbox
1388
1389 def _build_tc_page(self):
1390@@ -590,8 +600,6 @@
1391 self.verify_email_vbox.default_widget = self.verify_token_button
1392 self.verify_email_vbox.default_widget.set_flags(gtk.CAN_DEFAULT)
1393
1394- cb = lambda w: self.verify_token_button.clicked()
1395- self.email_token_entry.connect('activate', cb)
1396 self.verify_email_vbox.pack_start(self.email_token_entry, expand=False)
1397 self.verify_email_vbox.reorder_child(self.email_token_entry, 0)
1398
1399@@ -623,9 +631,6 @@
1400 self.forgotten_password_button.set_label(msg)
1401 self.login_ok_button.grab_focus()
1402
1403- for entry in (self.login_email_entry, self.login_password_entry):
1404- entry.connect('activate', lambda w: self.login_ok_button.clicked())
1405-
1406 return self.login_vbox
1407
1408 def _build_request_password_token_page(self):
1409@@ -645,9 +650,6 @@
1410 self.request_password_token_ok_button.set_label(self.NEXT)
1411 self.request_password_token_ok_button.set_sensitive(False)
1412
1413- cb = lambda w: self.request_password_token_ok_button.clicked()
1414- self.reset_email_entry.connect('activate', cb)
1415-
1416 return self.request_password_token_vbox
1417
1418 def _build_set_new_password_page(self):
1419@@ -658,12 +660,10 @@
1420 btn.set_flags(gtk.CAN_DEFAULT)
1421 self.set_new_password_vbox.default_widget = btn
1422
1423- cb = lambda w: self.set_new_password_ok_button.clicked()
1424 for entry in (self.reset_code_entry,
1425 self.reset_password1_entry,
1426 self.reset_password2_entry):
1427 self.set_new_password_details_vbox.pack_start(entry, expand=False)
1428- entry.connect('activate', cb)
1429
1430 cb = self.on_set_new_password_entries_changed
1431 self.reset_code_entry.connect('changed', cb)
1432@@ -725,8 +725,8 @@
1433 remove = self.bus.remove_signal_receiver
1434 for (iface, signal) in self._signals_receivers.keys():
1435 method = self._signals_receivers.pop((iface, signal))
1436- logger.info('Removing signal %r with method %r at iface %r.',
1437- signal, method, iface)
1438+ logger.debug('Removing signal %r with method %r at iface %r.',
1439+ signal, method, iface)
1440 remove(method, signal_name=signal, dbus_interface=iface)
1441
1442 # hide the main window
1443@@ -742,6 +742,7 @@
1444 elif len(self._gtk_signal_log) > 0:
1445 signal = self._gtk_signal_log[-1][0]
1446 args = self._gtk_signal_log[-1][1:]
1447+ logger.info('emitting %r with args %r.', signal, args)
1448 self.window.emit(signal, *args)
1449 else:
1450 self.window.emit(SIG_USER_CANCELATION, self.app_name)
1451@@ -812,14 +813,6 @@
1452 f(self.app_name, email1, password1, self._captcha_id, captcha_solution,
1453 reply_handler=NO_OP, error_handler=NO_OP)
1454
1455- def on_tc_button_clicked(self, *args, **kwargs):
1456- """T & C button was clicked, show the browser with them."""
1457- self._set_current_page(self.tc_browser_vbox)
1458-
1459- def on_tc_back_button_clicked(self, *args, **kwargs):
1460- """T & C 'back' button was clicked, return to the previous page."""
1461- self._set_current_page(self.enter_details_vbox)
1462-
1463 def on_verify_token_button_clicked(self, *args, **kwargs):
1464 """The user entered the email token, let's verify!"""
1465 if not self.verify_token_button.is_sensitive():
1466@@ -951,15 +944,60 @@
1467
1468 self._set_current_page(self.processing_vbox)
1469
1470- def on_tc_browser_vbox_show(self, *args, **kwargs):
1471- """The T&C page is being shown."""
1472+ def on_tc_button_clicked(self, *args, **kwargs):
1473+ """The T&C button was clicked, create the browser and load terms."""
1474+ # delay the import of webkit to be able to build without it
1475+ import webkit
1476 browser = webkit.WebView()
1477+
1478+ # The signal WebKitWebView::load-finished is deprecated and should not
1479+ # be used in newly-written code. Use the "load-status" property
1480+ # instead. Connect to "notify::load-status" to monitor loading.
1481+
1482+ # nataliabidart (2010-10-04): connecting this signal makes the loading
1483+ # of the Ubuntu One terms URL to fail. So we're using the deprecated
1484+ # 'load-finished' for now.
1485+
1486+ #browser.connect('notify::load-status',
1487+ # self.on_tc_browser_notify_load_status)
1488+ browser.connect('load-finished',
1489+ self.on_tc_browser_notify_load_status)
1490+ browser.connect('navigation-policy-decision-requested',
1491+ self.on_tc_browser_navigation_requested)
1492+
1493 settings = browser.get_settings()
1494 settings.set_property("enable-plugins", False)
1495 settings.set_property("enable-default-context-menu", False)
1496- browser.open(self.tc_uri)
1497+
1498+ # webkit_web_view_open has been deprecated since version 1.1.1 and
1499+ # should not be used in newly-written code. Use
1500+ # webkit_web_view_load_uri() instead.
1501+ browser.load_uri(self.tc_url)
1502 browser.show()
1503 self.tc_browser_window.add(browser)
1504+ self._set_current_page(self.processing_vbox)
1505+
1506+ def on_tc_back_button_clicked(self, *args, **kwargs):
1507+ """T & C 'back' button was clicked, return to the previous page."""
1508+ self._set_current_page(self.enter_details_vbox)
1509+
1510+ def on_tc_browser_notify_load_status(self, browser, *args, **kwargs):
1511+ """The T&C page is being loaded."""
1512+ if browser.get_load_status() == WEBKIT_LOAD_FINISHED:
1513+ self._set_current_page(self.tc_browser_vbox)
1514+
1515+ def on_tc_browser_navigation_requested(self, browser, frame, request,
1516+ action, decision, *args, **kwargs):
1517+ """The user wants to navigate within the T&C browser."""
1518+ if action is not None and \
1519+ action.get_reason() == WEBKIT_WEB_NAVIGATION_REASON_LINK_CLICKED:
1520+ if decision is not None:
1521+ decision.ignore()
1522+ url = action.get_original_uri()
1523+ webbrowser.open(url)
1524+ else:
1525+ if decision is not None:
1526+ decision.use()
1527
1528 def on_tc_browser_vbox_hide(self, *args, **kwargs):
1529 """The T&C page is no longer being shown."""
1530@@ -987,10 +1025,21 @@
1531 result = msg1 if msg1 is not None else msg2
1532 return result
1533
1534+ def _append_error_signal(self, signal, error):
1535+ """Append 'signal' to the log with a formatted version of 'error'.
1536+
1537+ Format DBus dict 'error' into a nice string.
1538+
1539+ """
1540+ msg = '\n'.join('%s: %s' % i for i in error.iteritems())
1541+ self._gtk_signal_log.append((signal, self.app_name, msg))
1542+
1543 @log_call
1544 def on_captcha_generated(self, app_name, captcha_id, *args, **kwargs):
1545 """Captcha image has been generated and is available to be shown."""
1546- assert captcha_id is not None
1547+ if captcha_id is None:
1548+ logger.warning('on_captcha_generated: captcha_id is None for '
1549+ 'app_name "%s".', app_name)
1550 self._captcha_id = captcha_id
1551 self._set_captcha_image()
1552
1553@@ -1021,9 +1070,7 @@
1554
1555 msg = self._build_general_error_message(error)
1556 self._set_current_page(self.enter_details_vbox, warning_text=msg)
1557-
1558- self._gtk_signal_log.append((SIG_REGISTRATION_FAILED, self.app_name,
1559- msg))
1560+ self._append_error_signal(SIG_REGISTRATION_FAILED, error)
1561
1562 @log_call
1563 def on_email_validated(self, app_name, email, *args, **kwargs):
1564@@ -1041,9 +1088,7 @@
1565
1566 msg = self._build_general_error_message(error)
1567 self._set_current_page(self.verify_email_vbox, warning_text=msg)
1568-
1569- self._gtk_signal_log.append((SIG_REGISTRATION_FAILED, self.app_name,
1570- msg))
1571+ self._append_error_signal(SIG_REGISTRATION_FAILED, error)
1572
1573 @log_call
1574 def on_logged_in(self, app_name, email, *args, **kwargs):
1575@@ -1057,8 +1102,7 @@
1576 """User was not successfully logged in."""
1577 msg = self._build_general_error_message(error)
1578 self._set_current_page(self.login_vbox, warning_text=msg)
1579- self._gtk_signal_log.append((SIG_LOGIN_FAILED, self.app_name,
1580- msg))
1581+ self._append_error_signal(SIG_LOGIN_FAILED, error)
1582
1583 @log_call
1584 def on_password_reset_token_sent(self, app_name, email, *args, **kwargs):
1585
1586=== modified file 'ubuntu_sso/keyring.py'
1587--- ubuntu_sso/keyring.py 2010-09-22 16:47:09 +0000
1588+++ ubuntu_sso/keyring.py 2010-10-18 13:21:21 +0000
1589@@ -1,3 +1,5 @@
1590+# -*- coding: utf-8 -*-
1591+#
1592 # Copyright (C) 2010 Canonical
1593 #
1594 # Authors:
1595
1596=== modified file 'ubuntu_sso/logger.py'
1597--- ubuntu_sso/logger.py 2010-09-08 19:25:02 +0000
1598+++ ubuntu_sso/logger.py 2010-10-18 13:21:21 +0000
1599@@ -1,3 +1,5 @@
1600+# -*- coding: utf-8 -*-
1601+#
1602 # ubuntu_sso.logger - logging miscellany
1603 #
1604 # Author: Stuart Langridge <stuart.langridge@canonical.com>
1605@@ -27,33 +29,32 @@
1606
1607 from logging.handlers import RotatingFileHandler
1608
1609+
1610 LOGFOLDER = os.path.join(xdg.BaseDirectory.xdg_cache_home, 'sso')
1611 # create log folder if it doesn't exists
1612 if not os.path.exists(LOGFOLDER):
1613 os.makedirs(LOGFOLDER)
1614
1615-LOGFILENAME = os.path.join(LOGFOLDER, 'oauth-login.log')
1616-FMT = "%(asctime)s:%(msecs)s - %(name)s - %(levelname)s - %(message)s"
1617-
1618 if os.environ.get('DEBUG'):
1619 LOG_LEVEL = logging.DEBUG
1620 else:
1621 # Only log this level and above
1622 LOG_LEVEL = logging.INFO
1623
1624+MAIN_HANDLER = RotatingFileHandler(os.path.join(LOGFOLDER, 'sso-client.log'),
1625+ maxBytes=1048576,
1626+ backupCount=5)
1627+MAIN_HANDLER.setLevel(LOG_LEVEL)
1628+FMT = "%(asctime)s:%(msecs)s - %(name)s - %(levelname)s - %(message)s"
1629+MAIN_HANDLER.setFormatter(logging.Formatter(fmt=FMT))
1630+
1631
1632 def setup_logging(log_domain):
1633- """Create basic logger to set filename"""
1634- root_formatter = logging.Formatter(fmt=FMT)
1635- root_handler = RotatingFileHandler(LOGFILENAME, maxBytes=1048576,
1636- backupCount=5)
1637- root_handler.setLevel(LOG_LEVEL)
1638- root_handler.setFormatter(root_formatter)
1639-
1640+ """Create basic logger to set filename."""
1641 logger = logging.getLogger(log_domain)
1642 logger.propagate = False
1643 logger.setLevel(LOG_LEVEL)
1644- logger.addHandler(root_handler)
1645+ logger.addHandler(MAIN_HANDLER)
1646 if os.environ.get('DEBUG'):
1647 debug_handler = logging.StreamHandler(sys.stderr)
1648 logger.addHandler(debug_handler)
1649
1650=== modified file 'ubuntu_sso/main.py'
1651--- ubuntu_sso/main.py 2010-09-14 19:28:09 +0000
1652+++ ubuntu_sso/main.py 2010-10-18 13:21:21 +0000
1653@@ -1,3 +1,5 @@
1654+# -*- coding: utf-8 -*-
1655+#
1656 # ubuntu_sso.main - main login handling interface
1657 #
1658 # Author: Natalia Bidart <natalia.bidart@canonical.com>
1659@@ -26,25 +28,18 @@
1660 """
1661
1662 import os
1663-import re
1664 import threading
1665-import traceback
1666-import urllib2
1667+import warnings
1668
1669 import dbus.service
1670-import gobject
1671-
1672-# Unable to import 'lazr.restfulclient.*'
1673-# pylint: disable=F0401
1674-from lazr.restfulclient.authorize import BasicHttpAuthorizer
1675-from lazr.restfulclient.authorize.oauth import OAuthAuthorizer
1676-from lazr.restfulclient.errors import HTTPError
1677-from lazr.restfulclient.resource import ServiceRoot
1678-# pylint: enable=F0401
1679-from oauth import oauth
1680-
1681-from ubuntu_sso import DBUS_IFACE_USER_NAME, DBUS_IFACE_CRED_NAME
1682-from ubuntu_sso.keyring import Keyring, get_token_name, U1_APP_NAME
1683+
1684+from ubuntu_sso import (DBUS_ACCOUNT_PATH, DBUS_IFACE_USER_NAME,
1685+ DBUS_IFACE_CRED_NAME, DBUS_CREDENTIALS_IFACE)
1686+from ubuntu_sso.account import Account
1687+from ubuntu_sso.credentials import (Credentials, keyring_store_credentials,
1688+ HELP_TEXT_KEY, PING_URL_KEY, TC_URL_KEY, WINDOW_ID_KEY,
1689+ SUCCESS_CB_KEY, ERROR_CB_KEY, DENIAL_CB_KEY, ERROR_KEY, ERROR_DETAIL_KEY)
1690+from ubuntu_sso.keyring import get_token_name, U1_APP_NAME
1691 from ubuntu_sso.logger import setup_logging
1692
1693
1694@@ -53,221 +48,21 @@
1695
1696
1697 logger = setup_logging("ubuntu_sso.main")
1698-PING_URL = "https://one.ubuntu.com/oauth/sso-finished-so-get-tokens/"
1699-SERVICE_URL = "https://login.ubuntu.com/api/1.0"
1700-
1701-
1702-class NoDefaultConfigError(Exception):
1703- """No default section in configuration file"""
1704-
1705-
1706-class BadRealmError(Exception):
1707- """Realm must be a URL."""
1708-
1709-
1710-class InvalidEmailError(Exception):
1711- """The email is not valid."""
1712-
1713-
1714-class InvalidPasswordError(Exception):
1715- """The password is not valid.
1716-
1717- Must provide at least 8 characters, one upper case, one number.
1718+U1_PING_URL = "https://one.ubuntu.com/oauth/sso-finished-so-get-tokens/"
1719+
1720+
1721+class SSOLoginProcessor(Account):
1722+ """Login and register users using the Ubuntu Single Sign On service.
1723+
1724+ Alias classname to maintain backwards compatibility. DO NOT USE, use
1725+ ubuntu_sso.account.Account instead.
1726 """
1727
1728-
1729-class RegistrationError(Exception):
1730- """The registration failed."""
1731-
1732-
1733-class AuthenticationError(Exception):
1734- """The authentication failed."""
1735-
1736-
1737-class EmailTokenError(Exception):
1738- """The email token is not valid."""
1739-
1740-
1741-class ResetPasswordTokenError(Exception):
1742- """The token for password reset could not be generated."""
1743-
1744-
1745-class NewPasswordError(Exception):
1746- """The new password could not be set."""
1747-
1748-
1749-def keyring_store_credentials(app_name, credentials):
1750- """Store the credentials in the keyring."""
1751- logger.info('keyring_store_credentials: app_name "%s".', app_name)
1752- Keyring(app_name).set_ubuntusso_attr(credentials)
1753-
1754-
1755-def keyring_get_credentials(app_name):
1756- """Get the credentials from the keyring or None if not there."""
1757- creds = Keyring(app_name).get_ubuntusso_attr()
1758- logger.info('keyring_get_credentials: app_name "%s", resulting credentials'
1759- ' is not None? %r', app_name, creds is not None)
1760- return creds
1761-
1762-
1763-class SSOLoginProcessor(object):
1764- """Login and register users using the Ubuntu Single Sign On service."""
1765-
1766 def __init__(self, sso_service_class=None):
1767- """Create a new SSO login processor."""
1768- if sso_service_class is None:
1769- self.sso_service_class = ServiceRoot
1770- else:
1771- self.sso_service_class = sso_service_class
1772-
1773- self.service_url = os.environ.get('USSOC_SERVICE_URL', SERVICE_URL)
1774-
1775- def _valid_email(self, email):
1776- """Validate the given email."""
1777- return email is not None and '@' in email
1778-
1779- def _valid_password(self, password):
1780- """Validate the given password."""
1781- res = (len(password) > 7 and # at least 8 characters
1782- re.search('[A-Z]', password) and # one upper case
1783- re.search('\d+', password)) # one number
1784- return res
1785-
1786- def _format_webservice_errors(self, errdict):
1787- """Turn each list of strings in the errdict into a LF separated str."""
1788- result = {}
1789- for k, v in errdict.iteritems():
1790- # workaround until bug #624955 is solved
1791- if isinstance(v, basestring):
1792- result[k] = v
1793- else:
1794- result[k] = "\n".join(v)
1795- return result
1796-
1797- def generate_captcha(self, filename):
1798- """Generate a captcha using the SSO service."""
1799- logger.debug('generate_captcha: requesting captcha, filename: %r',
1800- filename)
1801- sso_service = self.sso_service_class(None, self.service_url)
1802- captcha = sso_service.captchas.new()
1803-
1804- # download captcha and save to 'filename'
1805- logger.debug('generate_captcha: server answered: %r', captcha)
1806- try:
1807- res = urllib2.urlopen(captcha['image_url'])
1808- with open(filename, 'wb') as f:
1809- f.write(res.read())
1810- except:
1811- msg = 'generate_captcha crashed while downloading the image.'
1812- logger.exception(msg)
1813- raise
1814-
1815- return captcha['captcha_id']
1816-
1817- def register_user(self, email, password, captcha_id, captcha_solution):
1818- """Register a new user with 'email' and 'password'."""
1819- logger.debug('register_user: email: %r password: <hidden>, '
1820- 'captcha_id: %r, captcha_solution: %r',
1821- email, captcha_id, captcha_solution)
1822- sso_service = self.sso_service_class(None, self.service_url)
1823- if not self._valid_email(email):
1824- logger.error('register_user: InvalidEmailError for email: %r',
1825- email)
1826- raise InvalidEmailError()
1827- if not self._valid_password(password):
1828- logger.error('register_user: InvalidPasswordError')
1829- raise InvalidPasswordError()
1830-
1831- result = sso_service.registrations.register(
1832- email=email, password=password, captcha_id=captcha_id,
1833- captcha_solution=captcha_solution)
1834- logger.info('register_user: email: %r result: %r', email, result)
1835-
1836- if result['status'] == 'error':
1837- errorsdict = self._format_webservice_errors(result['errors'])
1838- raise RegistrationError(errorsdict)
1839- elif result['status'] != 'ok':
1840- raise RegistrationError('Received unknown status: %s' % result)
1841- else:
1842- return email
1843-
1844- def login(self, email, password, token_name):
1845- """Login a user with 'email' and 'password'."""
1846- logger.debug('login: email: %r password: <hidden>, token_name: %r',
1847- email, token_name)
1848- basic = BasicHttpAuthorizer(email, password)
1849- sso_service = self.sso_service_class(basic, self.service_url)
1850- service = sso_service.authentications.authenticate
1851-
1852- try:
1853- credentials = service(token_name=token_name)
1854- except HTTPError:
1855- logger.exception('login failed with:')
1856- raise AuthenticationError()
1857-
1858- logger.debug('login: authentication successful! consumer_key: %r, ' \
1859- 'token_name: %r', credentials['consumer_key'], token_name)
1860- return credentials
1861-
1862- def validate_email(self, email, password, email_token, token_name):
1863- """Validate an email token for user with 'email' and 'password'."""
1864- logger.debug('validate_email: email: %r password: <hidden>, '
1865- 'email_token: %r, token_name: %r.',
1866- email, email_token, token_name)
1867- token = self.login(email=email, password=password,
1868- token_name=token_name)
1869-
1870- oauth_token = oauth.OAuthToken(token['token'], token['token_secret'])
1871- authorizer = OAuthAuthorizer(token['consumer_key'],
1872- token['consumer_secret'],
1873- oauth_token)
1874- sso_service = self.sso_service_class(authorizer, self.service_url)
1875- result = sso_service.accounts.validate_email(email_token=email_token)
1876- logger.info('validate_email: email: %r result: %r', email, result)
1877- if 'errors' in result:
1878- errorsdict = self._format_webservice_errors(result['errors'])
1879- raise EmailTokenError(errorsdict)
1880- elif 'email' in result:
1881- return token
1882- else:
1883- raise EmailTokenError('Received invalid reply: %s' % result)
1884-
1885- def request_password_reset_token(self, email):
1886- """Request a token to reset the password for the account 'email'."""
1887- sso_service = self.sso_service_class(None, self.service_url)
1888- service = sso_service.registrations.request_password_reset_token
1889- try:
1890- result = service(email=email)
1891- except HTTPError, e:
1892- logger.exception('request_password_reset_token failed with:')
1893- raise ResetPasswordTokenError(e.content.split('\n')[0])
1894-
1895- if result['status'] == 'ok':
1896- return email
1897- else:
1898- raise ResetPasswordTokenError('Received invalid reply: %s' %
1899- result)
1900-
1901- def set_new_password(self, email, token, new_password):
1902- """Set a new password for the account 'email' to be 'new_password'.
1903-
1904- The 'token' has to be the one resulting from a call to
1905- 'request_password_reset_token'.
1906-
1907- """
1908- sso_service = self.sso_service_class(None, self.service_url)
1909- service = sso_service.registrations.set_new_password
1910- try:
1911- result = service(email=email, token=token,
1912- new_password=new_password)
1913- except HTTPError, e:
1914- logger.exception('set_new_password failed with:')
1915- raise NewPasswordError(e.content.split('\n')[0])
1916-
1917- if result['status'] == 'ok':
1918- return email
1919- else:
1920- raise NewPasswordError('Received invalid reply: %s' % result)
1921+ """Create a new SSO Account manager."""
1922+ msg = 'Use ubuntu_sso.account.Account instead.'
1923+ warnings.warn(msg, DeprecationWarning)
1924+ super(SSOLoginProcessor, self).__init__(sso_service_class)
1925
1926
1927 def except_to_errdict(e):
1928@@ -304,11 +99,11 @@
1929 # Operator not preceded by a space (fails with dbus decorators)
1930 # pylint: disable=C0322
1931
1932- def __init__(self, bus_name,
1933- sso_login_processor_class=SSOLoginProcessor,
1934+ def __init__(self, bus_name, object_path=DBUS_ACCOUNT_PATH,
1935+ sso_login_processor_class=Account,
1936 sso_service_class=None):
1937 """Initiate the Login object."""
1938- dbus.service.Object.__init__(self, object_path="/sso",
1939+ dbus.service.Object.__init__(self, object_path=object_path,
1940 bus_name=bus_name)
1941 self.sso_login_processor_class = sso_login_processor_class
1942 self.sso_service_class = sso_service_class
1943@@ -390,7 +185,6 @@
1944 credentials = self.processor().login(email, password, token_name)
1945 logger.debug('login returned not None credentials? %r.',
1946 credentials is not None)
1947- assert credentials is not None
1948 keyring_store_credentials(app_name, credentials)
1949 return email
1950 blocking(f, app_name, self.LoggedIn, self.LoginError)
1951@@ -476,103 +270,41 @@
1952
1953 def __init__(self, *args, **kwargs):
1954 dbus.service.Object.__init__(self, *args, **kwargs)
1955- self.ping_url = os.environ.get('USSOC_PING_URL', PING_URL)
1956+ self.ping_url = os.environ.get('USSOC_PING_URL', U1_PING_URL)
1957+
1958+ def _process_error(self, app_name, error_dict):
1959+ """Process the 'error_dict' and emit CredentialsError."""
1960+ msg = error_dict.get(ERROR_KEY, 'No error message given.')
1961+ detail = error_dict.get(ERROR_DETAIL_KEY, 'No detailed error given.')
1962+ self.CredentialsError(app_name, msg, detail)
1963
1964 @dbus.service.signal(DBUS_IFACE_CRED_NAME, signature="s")
1965 def AuthorizationDenied(self, app_name):
1966 """Signal thrown when the user denies the authorization."""
1967- logger.info('SSOLogin: emitting AuthorizationDenied with app_name '
1968- '"%s"', app_name)
1969+ logger.info('SSOCredentials: emitting AuthorizationDenied with '
1970+ 'app_name "%s"', app_name)
1971
1972 @dbus.service.signal(DBUS_IFACE_CRED_NAME, signature="sa{ss}")
1973 def CredentialsFound(self, app_name, credentials):
1974 """Signal thrown when the credentials are found."""
1975- logger.info('SSOLogin: emitting CredentialsFound with app_name "%s"',
1976- app_name)
1977+ logger.info('SSOCredentials: emitting CredentialsFound with '
1978+ 'app_name "%s"', app_name)
1979
1980 @dbus.service.signal(DBUS_IFACE_CRED_NAME, signature="sss")
1981 def CredentialsError(self, app_name, error_message, detailed_error):
1982 """Signal thrown when there is a problem finding the credentials."""
1983- logger.debug('SSOCredentials: emitting CredentialsError with app_name '
1984+ logger.error('SSOCredentials: emitting CredentialsError with app_name '
1985 '"%s" and error_message %r', app_name, error_message)
1986
1987 @dbus.service.method(dbus_interface=DBUS_IFACE_CRED_NAME,
1988 in_signature="s", out_signature="a{ss}")
1989 def find_credentials(self, app_name):
1990- """Get the credentials from the keyring or '' if not there."""
1991- token = keyring_get_credentials(app_name)
1992+ """Get the credentials from the keyring or {} if not there."""
1993+ obj = Credentials(app_name=app_name)
1994+ token = obj.find_credentials()
1995 logger.info('find_credentials: app_name "%s", result is {}? %s',
1996- app_name, token is None)
1997- if token is None:
1998- return {}
1999- else:
2000- return token
2001-
2002- def _login_success_cb(self, dialog, app_name, email):
2003- """Handles the response from the UI dialog."""
2004- logger.info('Login successful for app %r, email %r', app_name, email)
2005- try:
2006- creds = keyring_get_credentials(app_name)
2007- self._ping_url(app_name, email, creds)
2008- self.CredentialsFound(app_name, creds)
2009- except: # pylint: disable=W0702
2010- msg = "Problem getting the credentials from the keyring."
2011- logger.exception(msg)
2012- self.CredentialsError(app_name, msg, traceback.format_exc())
2013-
2014- def _login_error_cb(self, dialog, app_name, error):
2015- """Handles a problem in the UI."""
2016- logger.info('Login unsuccessful for app %r, error %r', app_name, error)
2017- msg = "Problem getting the credentials from the keyring."
2018- self.CredentialsError(app_name, msg, "no more info available")
2019-
2020- def _login_auth_denied_cb(self, dialog, app_name):
2021- """The user decides not to allow the registration or login."""
2022- self.AuthorizationDenied(app_name)
2023-
2024- def _ping_url(self, app_name, email, credentials):
2025- """Ping the given url."""
2026- logger.info('Maybe pinging server for app_name "%s"', app_name)
2027- if app_name == U1_APP_NAME:
2028- url = self.ping_url + email
2029- consumer = oauth.OAuthConsumer(credentials['consumer_key'],
2030- credentials['consumer_secret'])
2031- token = oauth.OAuthToken(credentials['token'],
2032- credentials['token_secret'])
2033- get_request = oauth.OAuthRequest.from_consumer_and_token
2034- oauth_req = get_request(oauth_consumer=consumer, token=token,
2035- http_method="GET", http_url=url)
2036- oauth_req.sign_request(oauth.OAuthSignatureMethod_HMAC_SHA1(),
2037- consumer, token)
2038- request = urllib2.Request(url, headers=oauth_req.to_header())
2039- logger.debug('Opening the ping url %s with urllib2.urlopen. ' \
2040- 'Request to: %s', PING_URL, request.get_full_url())
2041- response = urllib2.urlopen(request)
2042- logger.debug('Url opened. Response: %s.', response.code)
2043- return response.code
2044-
2045- def _show_login_or_register_ui(self, app_name, tc_url, help_text,
2046- win_id, login_only=False):
2047- """Shows the UI so the user can login or register."""
2048- try:
2049- # delay gui import to be able to function on non-graphical envs
2050- from ubuntu_sso import gui
2051- gui_app = gui.UbuntuSSOClientGUI(app_name, tc_url,
2052- help_text, win_id, login_only)
2053- gui_app.connect(gui.SIG_LOGIN_SUCCEEDED, self._login_success_cb)
2054- gui_app.connect(gui.SIG_LOGIN_FAILED, self._login_error_cb)
2055- gui_app.connect(gui.SIG_REGISTRATION_SUCCEEDED,
2056- self._login_success_cb)
2057- gui_app.connect(gui.SIG_REGISTRATION_FAILED, self._login_error_cb)
2058- gui_app.connect(gui.SIG_USER_CANCELATION,
2059- self._login_auth_denied_cb)
2060- except: # pylint: disable=W0702
2061- msg = '_show_login_or_register_ui failed when calling ' \
2062- 'gui.UbuntuSSOClientGUI(%r, %r, %r, %r, %r)'
2063- logger.exception(msg, app_name, tc_url, help_text,
2064- win_id, login_only)
2065- msg = "Problem opening the Ubuntu SSO user interface."
2066- self.CredentialsError(app_name, msg, traceback.format_exc())
2067+ app_name, token == {})
2068+ return token
2069
2070 @dbus.service.method(dbus_interface=DBUS_IFACE_CRED_NAME,
2071 in_signature="sssx", out_signature="")
2072@@ -589,24 +321,14 @@
2073 the GUI. If 0, no parent will be set.
2074
2075 """
2076- try:
2077- token = keyring_get_credentials(app_name)
2078- if token is None:
2079- gobject.idle_add(self._show_login_or_register_ui,
2080- app_name, terms_and_conditions_url,
2081- help_text, window_id)
2082- else:
2083- self.CredentialsFound(app_name, token)
2084- except: # pylint: disable=W0702
2085- msg = "Problem getting the credentials from the keyring."
2086- logger.exception(msg)
2087- self.CredentialsError(app_name, msg, traceback.format_exc())
2088-
2089- def _show_login_only_ui(self, app_name, help_text, win_id):
2090- """Shows the UI so the user can login."""
2091- tc_url = None # no terms and conditions when only logging in
2092- self._show_login_or_register_ui(app_name, tc_url, help_text,
2093- win_id, login_only=True)
2094+ ping_url = self.ping_url if app_name == U1_APP_NAME else None
2095+ obj = Credentials(app_name=app_name, ping_url=ping_url,
2096+ tc_url=terms_and_conditions_url,
2097+ help_text=help_text, window_id=window_id,
2098+ success_cb=self.CredentialsFound,
2099+ error_cb=self._process_error,
2100+ denial_cb=self.AuthorizationDenied)
2101+ obj.register()
2102
2103 @dbus.service.method(dbus_interface=DBUS_IFACE_CRED_NAME,
2104 in_signature="ssx", out_signature="")
2105@@ -620,17 +342,13 @@
2106 the GUI. If 0, no parent will be set.
2107
2108 """
2109- try:
2110- token = keyring_get_credentials(app_name)
2111- if token is None:
2112- gobject.idle_add(self._show_login_only_ui, app_name,
2113- help_text, window_id)
2114- else:
2115- self.CredentialsFound(app_name, token)
2116- except: # pylint: disable=W0702
2117- msg = "Problem getting the credentials from the keyring."
2118- logger.exception(msg)
2119- self.CredentialsError(app_name, msg, traceback.format_exc())
2120+ ping_url = self.ping_url if app_name == U1_APP_NAME else None
2121+ obj = Credentials(app_name=app_name, ping_url=ping_url, tc_url=None,
2122+ help_text=help_text, window_id=window_id,
2123+ success_cb=self.CredentialsFound,
2124+ error_cb=self._process_error,
2125+ denial_cb=self.AuthorizationDenied)
2126+ obj.login()
2127
2128 @dbus.service.method(dbus_interface=DBUS_IFACE_CRED_NAME,
2129 in_signature='s', out_signature='')
2130@@ -639,10 +357,133 @@
2131
2132 'app_name' is the name of the application.
2133 """
2134- try:
2135- creds = Keyring(app_name)
2136- creds.delete_ubuntusso_attr()
2137- except: # pylint: disable=W0702
2138- logger.exception(
2139- "problem removing credentials from keyring for %s",
2140- app_name)
2141+ obj = Credentials(app_name=app_name)
2142+ obj.clear_credentials()
2143+
2144+
2145+class CredentialsManagement(dbus.service.Object):
2146+ """DBus object that manages credentials.
2147+
2148+ Every exposed method in this class requires one mandatory argument:
2149+
2150+ - 'app_name': the name of the application. Will be displayed in the
2151+ GUI header, plus it will be used to find/build/clear tokens.
2152+
2153+ And accepts another parameter named 'args', which is a dictionary that
2154+ can contain the following:
2155+
2156+ - 'help_text': an explanatory text for the end-users, will be
2157+ shown below the header. This is an optional free text field.
2158+
2159+ - 'ping_url': the url to open after successful token retrieval. If
2160+ defined, the email will be attached to the url and will be pinged
2161+ with a OAuth-signed request.
2162+
2163+ - 'tc_url': the link to the Terms and Conditions page. If defined,
2164+ the checkbox to agree to the terms will link to it.
2165+
2166+ - 'window_id': the id of the window which will be set as a parent
2167+ of the GUI. If not defined, no parent will be set.
2168+
2169+ """
2170+
2171+ # Operator not preceded by a space (fails with dbus decorators)
2172+ # pylint: disable=C0322
2173+
2174+ def _parse_args(self, args):
2175+ """Retrieve values from the generic param 'args'."""
2176+ result = dict((k, v) for k, v in args.iteritems() if k in
2177+ (HELP_TEXT_KEY, PING_URL_KEY, TC_URL_KEY, WINDOW_ID_KEY))
2178+ result[WINDOW_ID_KEY] = int(args.get(WINDOW_ID_KEY, 0))
2179+ result[SUCCESS_CB_KEY] = self.CredentialsFound
2180+ result[ERROR_CB_KEY] = self.CredentialsError
2181+ result[DENIAL_CB_KEY] = self.AuthorizationDenied
2182+ return result
2183+
2184+ @dbus.service.signal(DBUS_CREDENTIALS_IFACE, signature='s')
2185+ def AuthorizationDenied(self, app_name):
2186+ """Signal thrown when the user denies the authorization."""
2187+ logger.info('%s: emitting AuthorizationDenied with app_name "%s".',
2188+ self.__class__.__name__, app_name)
2189+
2190+ @dbus.service.signal(DBUS_CREDENTIALS_IFACE, signature='sa{ss}')
2191+ def CredentialsFound(self, app_name, credentials):
2192+ """Signal thrown when the credentials are found."""
2193+ logger.info('%s: emitting CredentialsFound with app_name "%s".',
2194+ self.__class__.__name__, app_name)
2195+
2196+ @dbus.service.signal(DBUS_CREDENTIALS_IFACE, signature='s')
2197+ def CredentialsNotFound(self, app_name):
2198+ """Signal thrown when the credentials are not found."""
2199+ logger.info('%s: emitting CredentialsNotFound with app_name "%s".',
2200+ self.__class__.__name__, app_name)
2201+
2202+ @dbus.service.signal(DBUS_CREDENTIALS_IFACE, signature='s')
2203+ def CredentialsCleared(self, app_name):
2204+ """Signal thrown when the credentials were cleared."""
2205+ logger.info('%s: emitting CredentialsCleared with app_name "%s".',
2206+ self.__class__.__name__, app_name)
2207+
2208+ @dbus.service.signal(DBUS_CREDENTIALS_IFACE, signature='sa{ss}')
2209+ def CredentialsError(self, app_name, error_dict):
2210+ """Signal thrown when there is a problem getting the credentials."""
2211+ logger.error('%s: emitting CredentialsError with app_name "%s" and '
2212+ 'error_dict %r.', self.__class__.__name__, app_name,
2213+ error_dict)
2214+
2215+ @dbus.service.method(dbus_interface=DBUS_CREDENTIALS_IFACE,
2216+ in_signature='sa{ss}', out_signature='')
2217+ def find_credentials(self, app_name, args):
2218+ """Look for the credentials for an application.
2219+
2220+ - 'app_name': the name of the application which credentials are
2221+ going to be removed.
2222+
2223+ - 'args' is a dictionary, currently not used.
2224+
2225+ """
2226+ obj = Credentials(app_name)
2227+
2228+ def success_cb(app, token):
2229+ """Find credentials and notify using signals."""
2230+ if token is not None and len(token) > 0:
2231+ self.CredentialsFound(app, token)
2232+ else:
2233+ self.CredentialsNotFound(app)
2234+
2235+ blocking(obj.find_credentials, app_name,
2236+ success_cb, self.CredentialsError)
2237+
2238+ @dbus.service.method(dbus_interface=DBUS_CREDENTIALS_IFACE,
2239+ in_signature='sa{ss}', out_signature='')
2240+ def clear_credentials(self, app_name, args):
2241+ """Clear the token for an application.
2242+
2243+ - 'app_name': the name of the application which credentials are
2244+ going to be removed.
2245+
2246+ - 'args' is a dictionary, currently not used.
2247+
2248+ """
2249+ obj = Credentials(app_name)
2250+
2251+ def success_cb(app_name, result):
2252+ """Clear credentials and notify using signals."""
2253+ self.CredentialsCleared(app_name)
2254+
2255+ blocking(obj.clear_credentials, app_name,
2256+ success_cb, self.CredentialsError)
2257+
2258+ @dbus.service.method(dbus_interface=DBUS_CREDENTIALS_IFACE,
2259+ in_signature='sa{ss}', out_signature='')
2260+ def register(self, app_name, args):
2261+ """Get credentials if found else prompt GUI to register."""
2262+ obj = Credentials(app_name, **self._parse_args(args))
2263+ obj.register()
2264+
2265+ @dbus.service.method(dbus_interface=DBUS_CREDENTIALS_IFACE,
2266+ in_signature='sa{ss}', out_signature='')
2267+ def login(self, app_name, args):
2268+ """Get credentials if found else prompt GUI to login."""
2269+ obj = Credentials(app_name, **self._parse_args(args))
2270+ obj.login()
2271
2272=== modified file 'ubuntu_sso/networkstate.py'
2273--- ubuntu_sso/networkstate.py 2010-09-08 19:25:02 +0000
2274+++ ubuntu_sso/networkstate.py 2010-10-18 13:21:21 +0000
2275@@ -1,3 +1,5 @@
2276+# -*- coding: utf-8 -*-
2277+#
2278 # networkstate - detect the current state of the network
2279 #
2280 # Author: Alejandro J. Cura <alecu@canonical.com>
2281
2282=== modified file 'ubuntu_sso/tests/__init__.py'
2283--- ubuntu_sso/tests/__init__.py 2010-06-16 15:11:04 +0000
2284+++ ubuntu_sso/tests/__init__.py 2010-10-18 13:21:21 +0000
2285@@ -1,3 +1,5 @@
2286+# -*- coding: utf-8 -*-
2287+#
2288 # Copyright 2009 Canonical Ltd.
2289 #
2290 # This program is free software: you can redistribute it and/or modify it
2291@@ -12,4 +14,32 @@
2292 # You should have received a copy of the GNU General Public License along
2293 # with this program. If not, see <http://www.gnu.org/licenses/>.
2294
2295-"""Tests for OAuth library."""
2296+"""Tests for the Ubuntu SSO library."""
2297+
2298+import os
2299+
2300+from ubuntu_sso.keyring import get_token_name
2301+
2302+APP_NAME = 'The Super App!'
2303+CAPTCHA_ID = 'test'
2304+CAPTCHA_PATH = os.path.abspath(os.path.join(os.curdir, 'ubuntu_sso', 'tests',
2305+ 'files', 'captcha.png'))
2306+CAPTCHA_SOLUTION = 'william Byrd'
2307+EMAIL = 'test@example.com'
2308+EMAIL_TOKEN = 'B2Pgtf'
2309+HELP_TEXT = """Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam sed
2310+lorem nibh. Suspendisse gravida nulla non nunc suscipit pulvinar tempus ut
2311+augue. Morbi consequat, ligula a elementum pretium, dolor nulla tempus metus,
2312+sed viverra nisi risus non velit."""
2313+NAME = 'Juanito Pérez'
2314+PASSWORD = 'h3lloWorld'
2315+PING_URL = 'http://localhost/ping-me/'
2316+RESET_PASSWORD_TOKEN = '8G5Wtq'
2317+TOKEN = {u'consumer_key': u'xQ7xDAz',
2318+ u'consumer_secret': u'KzCJWCTNbbntwfyCKKjomJDzlgqxLy',
2319+ u'token_name': u'test',
2320+ u'token': u'GkInOfSMGwTXAUoVQwLUoPxElEEUdhsLVNTPhxHJDUIeHCPNEo',
2321+ u'token_secret': u'qFYImEtlczPbsCnYyuwLoPDlPEnvNcIktZphPQklAWrvyfFMV'}
2322+TOKEN_NAME = get_token_name(APP_NAME)
2323+TC_URL = 'http://localhost/'
2324+WINDOW_ID = 5
2325
2326=== modified file 'ubuntu_sso/tests/bin/show_gui'
2327--- ubuntu_sso/tests/bin/show_gui 2010-09-08 19:25:02 +0000
2328+++ ubuntu_sso/tests/bin/show_gui 2010-10-18 13:21:21 +0000
2329@@ -26,7 +26,7 @@
2330
2331
2332 APP_NAME = 'Ubuntu'
2333-TC_URI = 'http://one.ubuntu.com/terms'
2334+TC_URL = 'http://one.ubuntu.com/terms'
2335 HELP_TEXT = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. ' \
2336 'Nam sed lorem nibh. Suspendisse gravida nulla non nunc suscipit' \
2337 ' pulvinar tempus ut augue.'
2338@@ -35,13 +35,13 @@
2339
2340
2341 if __name__ == '__main__':
2342- tc_uri=TC_URI
2343+ tc_url=TC_URL
2344 login_only = False
2345 xid = 0
2346
2347 if '--login' in sys.argv:
2348 login_only = True
2349- tc_uri = None
2350+ tc_url = None
2351
2352 if '--with-parent-window' in sys.argv:
2353 win = gtk.Window()
2354@@ -52,6 +52,6 @@
2355 xid = win.window.xid
2356
2357 UbuntuSSOClientGUI(close_callback=main_quit, app_name=APP_NAME,
2358- tc_uri=tc_uri, help_text=HELP_TEXT, window_id=xid,
2359+ tc_url=tc_url, help_text=HELP_TEXT, window_id=xid,
2360 login_only=login_only)
2361 gtk.main()
2362
2363=== added file 'ubuntu_sso/tests/test_account.py'
2364--- ubuntu_sso/tests/test_account.py 1970-01-01 00:00:00 +0000
2365+++ ubuntu_sso/tests/test_account.py 2010-10-18 13:21:21 +0000
2366@@ -0,0 +1,335 @@
2367+# -*- coding: utf-8 -*-
2368+#
2369+# Author: Natalia Bidart <natalia.bidart@canonical.com>
2370+#
2371+# Copyright 2010 Canonical Ltd.
2372+#
2373+# This program is free software: you can redistribute it and/or modify it
2374+# under the terms of the GNU General Public License version 3, as published
2375+# by the Free Software Foundation.
2376+#
2377+# This program is distributed in the hope that it will be useful, but
2378+# WITHOUT ANY WARRANTY; without even the implied warranties of
2379+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
2380+# PURPOSE. See the GNU General Public License for more details.
2381+#
2382+# You should have received a copy of the GNU General Public License along
2383+# with this program. If not, see <http://www.gnu.org/licenses/>.
2384+"""Tests for the SSO account code."""
2385+
2386+import os
2387+
2388+# Unable to import 'lazr.restfulclient.*'
2389+# pylint: disable=F0401
2390+from lazr.restfulclient.errors import HTTPError
2391+# pylint: enable=F0401
2392+from twisted.trial.unittest import TestCase
2393+
2394+from ubuntu_sso.account import (Account, AuthenticationError, EmailTokenError,
2395+ InvalidEmailError, InvalidPasswordError, NewPasswordError, SERVICE_URL,
2396+ RegistrationError, ResetPasswordTokenError)
2397+from ubuntu_sso.tests import (APP_NAME, CAPTCHA_ID, CAPTCHA_PATH,
2398+ CAPTCHA_SOLUTION, EMAIL, EMAIL_TOKEN, PASSWORD, RESET_PASSWORD_TOKEN,
2399+ TOKEN, TOKEN_NAME)
2400+
2401+
2402+CANT_RESET_PASSWORD_CONTENT = "CanNotResetPassowrdError: " \
2403+ "Can't reset password for this account"
2404+RESET_TOKEN_INVALID_CONTENT = "AuthToken matching query does not exist."
2405+EMAIL_ALREADY_REGISTERED = 'a@example.com'
2406+STATUS_UNKNOWN = {'status': 'yadda-yadda'}
2407+STATUS_ERROR = {'status': 'error', 'errors': {'something': ['Bla', 'Ble']}}
2408+STATUS_OK = {'status': 'ok'}
2409+STATUS_EMAIL_UNKNOWN = {'status': 'yadda-yadda'}
2410+STATUS_EMAIL_ERROR = {'errors': {'email_token': ['Error1', 'Error2']}}
2411+STATUS_EMAIL_OK = {'email': EMAIL}
2412+
2413+
2414+class FakedCaptchas(object):
2415+ """Fake the captcha generator."""
2416+
2417+ def new(self):
2418+ """Return a fix captcha)."""
2419+ return {'image_url': 'file://%s' % CAPTCHA_PATH,
2420+ 'captcha_id': CAPTCHA_ID}
2421+
2422+
2423+class FakedRegistrations(object):
2424+ """Fake the registrations service."""
2425+
2426+ def register(self, email, password, captcha_id, captcha_solution):
2427+ """Fake registration. Return a fix result."""
2428+ if email == EMAIL_ALREADY_REGISTERED:
2429+ return {'status': 'error',
2430+ 'errors': {'email': 'Email already registered'}}
2431+ elif captcha_id is None and captcha_solution is None:
2432+ return STATUS_UNKNOWN
2433+ elif captcha_id != CAPTCHA_ID or captcha_solution != CAPTCHA_SOLUTION:
2434+ return STATUS_ERROR
2435+ else:
2436+ return STATUS_OK
2437+
2438+ def request_password_reset_token(self, email):
2439+ """Fake password reset token. Return a fix result."""
2440+ if email is None:
2441+ return STATUS_UNKNOWN
2442+ elif email != EMAIL:
2443+ raise HTTPError(response=None, content=CANT_RESET_PASSWORD_CONTENT)
2444+ else:
2445+ return STATUS_OK
2446+
2447+ def set_new_password(self, email, token, new_password):
2448+ """Fake the setting of new password. Return a fix result."""
2449+ if email is None and token is None and new_password is None:
2450+ return STATUS_UNKNOWN
2451+ elif email != EMAIL or token != RESET_PASSWORD_TOKEN:
2452+ raise HTTPError(response=None, content=RESET_TOKEN_INVALID_CONTENT)
2453+ else:
2454+ return STATUS_OK
2455+
2456+
2457+class FakedAuthentications(object):
2458+ """Fake the authentications service."""
2459+
2460+ def authenticate(self, token_name):
2461+ """Fake authenticate. Return a fix result."""
2462+ if not token_name.startswith(TOKEN_NAME):
2463+ raise HTTPError(response=None, content=None)
2464+ else:
2465+ return TOKEN
2466+
2467+
2468+class FakedAccounts(object):
2469+ """Fake the accounts service."""
2470+
2471+ def validate_email(self, email_token):
2472+ """Fake the email validation. Return a fix result."""
2473+ if email_token is None:
2474+ return STATUS_EMAIL_UNKNOWN
2475+ elif email_token == EMAIL_ALREADY_REGISTERED:
2476+ return {'status': 'error',
2477+ 'errors': {'email': 'Email already registered'}}
2478+ elif email_token != EMAIL_TOKEN:
2479+ return STATUS_EMAIL_ERROR
2480+ else:
2481+ return STATUS_EMAIL_OK
2482+
2483+
2484+class FakedSSOServer(object):
2485+ """Fake an SSO server."""
2486+
2487+ def __init__(self, authorizer, service_root):
2488+ self.captchas = FakedCaptchas()
2489+ self.registrations = FakedRegistrations()
2490+ self.authentications = FakedAuthentications()
2491+ self.accounts = FakedAccounts()
2492+
2493+
2494+class AccountTestCase(TestCase):
2495+ """Test suite for the SSO login processor."""
2496+
2497+ def setUp(self):
2498+ """Init."""
2499+ self.processor = Account(sso_service_class=FakedSSOServer)
2500+ self.register_kwargs = dict(email=EMAIL, password=PASSWORD,
2501+ captcha_id=CAPTCHA_ID,
2502+ captcha_solution=CAPTCHA_SOLUTION)
2503+ self.login_kwargs = dict(email=EMAIL, password=PASSWORD,
2504+ token_name=TOKEN_NAME)
2505+
2506+ def tearDown(self):
2507+ """Clean up."""
2508+ self.processor = None
2509+
2510+ def test_generate_captcha(self):
2511+ """Captcha can be generated."""
2512+ filename = self.mktemp()
2513+ self.addCleanup(lambda: os.remove(filename))
2514+ captcha_id = self.processor.generate_captcha(filename)
2515+ self.assertEqual(CAPTCHA_ID, captcha_id, 'captcha id must be correct.')
2516+ self.assertTrue(os.path.isfile(filename), '%s must exist.' % filename)
2517+
2518+ with open(CAPTCHA_PATH) as f:
2519+ expected = f.read()
2520+ with open(filename) as f:
2521+ actual = f.read()
2522+ self.assertEqual(expected, actual, 'captcha image must be correct.')
2523+
2524+ def test_register_user_checks_valid_email(self):
2525+ """Email is validated."""
2526+ self.register_kwargs['email'] = 'notavalidemail'
2527+ self.assertRaises(InvalidEmailError,
2528+ self.processor.register_user, **self.register_kwargs)
2529+
2530+ def test_register_user_checks_valid_password(self):
2531+ """Password is validated."""
2532+ self.register_kwargs['password'] = ''
2533+ self.assertRaises(InvalidPasswordError,
2534+ self.processor.register_user, **self.register_kwargs)
2535+
2536+ # 7 chars, one less than expected
2537+ self.register_kwargs['password'] = 'tesT3it'
2538+ self.assertRaises(InvalidPasswordError,
2539+ self.processor.register_user, **self.register_kwargs)
2540+
2541+ self.register_kwargs['password'] = 'test3it!' # no upper case
2542+ self.assertRaises(InvalidPasswordError,
2543+ self.processor.register_user, **self.register_kwargs)
2544+
2545+ self.register_kwargs['password'] = 'testIt!!' # no number
2546+ self.assertRaises(InvalidPasswordError,
2547+ self.processor.register_user, **self.register_kwargs)
2548+
2549+ # register
2550+
2551+ def test_register_user_if_status_ok(self):
2552+ """A user is succesfuy registered into the SSO server."""
2553+ result = self.processor.register_user(**self.register_kwargs)
2554+ self.assertEqual(EMAIL, result, 'registration was successful.')
2555+
2556+ def test_register_user_if_status_error(self):
2557+ """Proper error is raised if register fails."""
2558+ self.register_kwargs['captcha_id'] = CAPTCHA_ID * 2 # incorrect
2559+ failure = self.assertRaises(RegistrationError,
2560+ self.processor.register_user,
2561+ **self.register_kwargs)
2562+ for k, val in failure.args[0].items():
2563+ self.assertIn(k, STATUS_ERROR['errors'])
2564+ self.assertEqual(val, "\n".join(STATUS_ERROR['errors'][k]))
2565+
2566+ def test_register_user_if_status_error_with_string_message(self):
2567+ """Proper error is raised if register fails."""
2568+ self.register_kwargs['email'] = EMAIL_ALREADY_REGISTERED
2569+ failure = self.assertRaises(RegistrationError,
2570+ self.processor.register_user,
2571+ **self.register_kwargs)
2572+ for k, val in failure.args[0].items():
2573+ self.assertIn(k, {'email': 'Email already registered'})
2574+ self.assertEqual(val, 'Email already registered')
2575+
2576+ def test_register_user_if_status_unknown(self):
2577+ """Proper error is raised if register returns an unknown status."""
2578+ self.register_kwargs['captcha_id'] = None
2579+ self.register_kwargs['captcha_solution'] = None
2580+ failure = self.assertRaises(RegistrationError,
2581+ self.processor.register_user,
2582+ **self.register_kwargs)
2583+ self.assertIn('Received unknown status: %s' % STATUS_UNKNOWN, failure)
2584+
2585+ # login
2586+
2587+ def test_login_if_http_error(self):
2588+ """Proper error is raised if authentication fails."""
2589+ self.login_kwargs['token_name'] = APP_NAME * 2 # invalid token name
2590+ self.assertRaises(AuthenticationError,
2591+ self.processor.login, **self.login_kwargs)
2592+
2593+ def test_login_if_no_error(self):
2594+ """A user can be succesfully logged in into the SSO service."""
2595+ result = self.processor.login(**self.login_kwargs)
2596+ self.assertEqual(TOKEN, result, 'authentication was successful.')
2597+
2598+ # validate_email
2599+
2600+ def test_validate_email_if_status_ok(self):
2601+ """A email is succesfuy validated in the SSO server."""
2602+ self.login_kwargs['email_token'] = EMAIL_TOKEN # valid email token
2603+ result = self.processor.validate_email(**self.login_kwargs)
2604+ self.assertEqual(TOKEN, result, 'email validation was successful.')
2605+
2606+ def test_validate_email_if_status_error(self):
2607+ """Proper error is raised if email validation fails."""
2608+ self.login_kwargs['email_token'] = EMAIL_TOKEN * 2 # invalid token
2609+ failure = self.assertRaises(EmailTokenError,
2610+ self.processor.validate_email,
2611+ **self.login_kwargs)
2612+ for k, val in failure.args[0].items():
2613+ self.assertIn(k, STATUS_EMAIL_ERROR['errors'])
2614+ self.assertEqual(val, "\n".join(STATUS_EMAIL_ERROR['errors'][k]))
2615+
2616+ def test_validate_email_if_status_error_with_string_message(self):
2617+ """Proper error is raised if register fails."""
2618+ self.login_kwargs['email_token'] = EMAIL_ALREADY_REGISTERED
2619+ failure = self.assertRaises(EmailTokenError,
2620+ self.processor.validate_email,
2621+ **self.login_kwargs)
2622+ for k, val in failure.args[0].items():
2623+ self.assertIn(k, {'email': 'Email already registered'})
2624+ self.assertEqual(val, 'Email already registered')
2625+
2626+ def test_validate_email_if_status_unknown(self):
2627+ """Proper error is raised if email validation returns unknown."""
2628+ self.login_kwargs['email_token'] = None
2629+ failure = self.assertRaises(EmailTokenError,
2630+ self.processor.validate_email,
2631+ **self.login_kwargs)
2632+ self.assertIn('Received invalid reply: %s' % STATUS_UNKNOWN, failure)
2633+
2634+ # reset_password
2635+
2636+ def test_request_password_reset_token_if_status_ok(self):
2637+ """A reset password token is succesfuly sent."""
2638+ result = self.processor.request_password_reset_token(email=EMAIL)
2639+ self.assertEqual(EMAIL, result,
2640+ 'password reset token must be successful.')
2641+
2642+ def test_request_password_reset_token_if_http_error(self):
2643+ """Proper error is raised if password token request fails."""
2644+ exc = self.assertRaises(ResetPasswordTokenError,
2645+ self.processor.request_password_reset_token,
2646+ email=EMAIL * 2)
2647+ self.assertIn(CANT_RESET_PASSWORD_CONTENT, exc)
2648+
2649+ def test_request_password_reset_token_if_status_unknown(self):
2650+ """Proper error is raised if password token request returns unknown."""
2651+ exc = self.assertRaises(ResetPasswordTokenError,
2652+ self.processor.request_password_reset_token,
2653+ email=None)
2654+ self.assertIn('Received invalid reply: %s' % STATUS_UNKNOWN, exc)
2655+
2656+ def test_set_new_password_if_status_ok(self):
2657+ """A new password is succesfuy set."""
2658+ result = self.processor.set_new_password(email=EMAIL,
2659+ token=RESET_PASSWORD_TOKEN,
2660+ new_password=PASSWORD)
2661+ self.assertEqual(EMAIL, result,
2662+ 'new password must be set successfully.')
2663+
2664+ def test_set_new_password_if_http_error(self):
2665+ """Proper error is raised if setting a new password fails."""
2666+ exc = self.assertRaises(NewPasswordError,
2667+ self.processor.set_new_password,
2668+ email=EMAIL * 2,
2669+ token=RESET_PASSWORD_TOKEN * 2,
2670+ new_password=PASSWORD)
2671+ self.assertIn(RESET_TOKEN_INVALID_CONTENT, exc)
2672+
2673+ def test_set_new_password_if_status_unknown(self):
2674+ """Proper error is raised if setting a new password returns unknown."""
2675+ exc = self.assertRaises(NewPasswordError,
2676+ self.processor.set_new_password,
2677+ email=None, token=None, new_password=None)
2678+ self.assertIn('Received invalid reply: %s' % STATUS_UNKNOWN, exc)
2679+
2680+
2681+class EnvironOverridesTestCase(TestCase):
2682+ """Some URLs can be set from the environment for testing/QA purposes."""
2683+
2684+ def test_override_service_url(self):
2685+ """The service url can be set from the env var USSOC_SERVICE_URL."""
2686+ fake_url = 'this is not really a URL'
2687+ old_url = os.environ.get('USSOC_SERVICE_URL')
2688+ os.environ['USSOC_SERVICE_URL'] = fake_url
2689+ try:
2690+ proc = Account(sso_service_class=FakedSSOServer)
2691+ self.assertEqual(proc.service_url, fake_url)
2692+ finally:
2693+ if old_url:
2694+ os.environ['USSOC_SERVICE_URL'] = old_url
2695+ else:
2696+ del os.environ['USSOC_SERVICE_URL']
2697+
2698+ def test_no_override_service_url(self):
2699+ """If the environ is unset, the default service url is used."""
2700+ proc = Account(sso_service_class=FakedSSOServer)
2701+ self.assertEqual(proc.service_url, SERVICE_URL)
2702
2703=== added file 'ubuntu_sso/tests/test_credentials.py'
2704--- ubuntu_sso/tests/test_credentials.py 1970-01-01 00:00:00 +0000
2705+++ ubuntu_sso/tests/test_credentials.py 2010-10-18 13:21:21 +0000
2706@@ -0,0 +1,444 @@
2707+# -*- coding: utf-8 -*-
2708+
2709+# Author: Natalia Bidart <natalia.bidart@canonical.com>
2710+#
2711+# Copyright 2010 Canonical Ltd.
2712+#
2713+# This program is free software: you can redistribute it and/or modify it
2714+# under the terms of the GNU General Public License version 3, as published
2715+# by the Free Software Foundation.
2716+#
2717+# This program is distributed in the hope that it will be useful, but
2718+# WITHOUT ANY WARRANTY; without even the implied warranties of
2719+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
2720+# PURPOSE. See the GNU General Public License for more details.
2721+#
2722+# You should have received a copy of the GNU General Public License along
2723+# with this program. If not, see <http://www.gnu.org/licenses/>.
2724+"""Tests for the Credentials module."""
2725+
2726+import logging
2727+import urllib
2728+
2729+import gobject
2730+
2731+from twisted.trial.unittest import TestCase, FailTest
2732+
2733+from contrib.testing.testcase import MementoHandler
2734+from ubuntu_sso import credentials, gui
2735+from ubuntu_sso.credentials import (APP_NAME_KEY, HELP_TEXT_KEY, NO_OP,
2736+ PING_URL_KEY, TC_URL_KEY, WINDOW_ID_KEY, ERROR_KEY, ERROR_DETAIL_KEY)
2737+from ubuntu_sso.tests import (APP_NAME, EMAIL, HELP_TEXT, PING_URL, TC_URL,
2738+ TOKEN, WINDOW_ID)
2739+
2740+
2741+# Access to a protected member of a client class
2742+# pylint: disable=W0212
2743+
2744+# Attribute defined outside __init__
2745+# pylint: disable=W0201
2746+
2747+
2748+KWARGS = {
2749+ APP_NAME_KEY: APP_NAME,
2750+ TC_URL_KEY: TC_URL,
2751+ HELP_TEXT_KEY: HELP_TEXT,
2752+ WINDOW_ID_KEY: WINDOW_ID,
2753+ PING_URL_KEY: PING_URL,
2754+}
2755+
2756+
2757+class BasicTestCase(TestCase):
2758+ """Test case with a helper tracker."""
2759+
2760+ def setUp(self):
2761+ """Init."""
2762+ self.patch(gobject, 'idle_add', lambda f, *a, **kw: f(*a, **kw))
2763+ self._called = False # helper
2764+
2765+ self.memento = MementoHandler()
2766+ self.memento.setLevel(logging.DEBUG)
2767+ credentials.logger.addHandler(self.memento)
2768+
2769+ def tearDown(self):
2770+ """Clean up."""
2771+ self._called = False
2772+
2773+ def _set_called(self, *args, **kwargs):
2774+ """Set _called to True."""
2775+ self._called = (args, kwargs)
2776+
2777+
2778+class CredentialsTestCase(BasicTestCase):
2779+ """Test suite for the Credentials class."""
2780+
2781+ def setUp(self):
2782+ """Init."""
2783+ super(CredentialsTestCase, self).setUp()
2784+ self.args = None
2785+ self.kwargs = None
2786+ self.signals = []
2787+
2788+ class FakedUbuntuSSOClientGUI(object):
2789+ """Fake a SSO GUI."""
2790+
2791+ # Method should have 'self' as first argument
2792+ # pylint: disable=E0213
2793+
2794+ def __init__(sself, *args, **kwargs):
2795+ self.args = args
2796+ self.kwargs = kwargs
2797+ sself.connect = lambda *a: self.signals.append(a)
2798+
2799+ self.patch(credentials.keyring.gnomekeyring, 'is_available',
2800+ lambda: True)
2801+ self.patch(gui, 'UbuntuSSOClientGUI', FakedUbuntuSSOClientGUI)
2802+ self.obj = credentials.Credentials(success_cb=self.success,
2803+ error_cb=self.error,
2804+ denial_cb=self.denial,
2805+ **KWARGS)
2806+
2807+ def tearDown(self):
2808+ """Clean up."""
2809+ super(CredentialsTestCase, self).tearDown()
2810+
2811+ def success(self, *args, **kwargs):
2812+ """To be called on success."""
2813+ self._set_called('success', *args, **kwargs)
2814+
2815+ def error(self, *args, **kwargs):
2816+ """To be called on error."""
2817+ self._set_called('error', *args, **kwargs)
2818+
2819+ def denial(self, *args, **kwargs):
2820+ """To be called on credentials denial."""
2821+ self._set_called('denial', *args, **kwargs)
2822+
2823+ def assert_error_cb_called(self, msg, detailed_error=None):
2824+ """Check that self.error_cb was called with proper arguments."""
2825+ self.assertEqual(len(self._called), 2)
2826+ self.assertEqual(self._called[0][0], 'error')
2827+ self.assertEqual(self._called[0][1], APP_NAME)
2828+ error_dict = self._called[0][2]
2829+ self.assertEqual(error_dict[ERROR_KEY], msg)
2830+ if detailed_error is not None:
2831+ self.assertIn(str(detailed_error), error_dict[ERROR_DETAIL_KEY])
2832+ else:
2833+ self.assertNotIn(ERROR_DETAIL_KEY, error_dict)
2834+ self.assertEqual(self._called[1], {})
2835+
2836+
2837+class CredentialsCallbacksTestCase(CredentialsTestCase):
2838+ """Test suite for the Credentials callbacks."""
2839+
2840+ def test_callbacks_are_stored(self):
2841+ """Creation callbacks are stored."""
2842+ self.assertEqual(self.obj.success_cb, self.success)
2843+ self.assertEqual(self.obj.error_cb, self.error)
2844+ self.assertEqual(self.obj.denial_cb, self.denial)
2845+
2846+ def test_callbacks_default_to_no_op(self):
2847+ """The callbacks are a no-operation if not given."""
2848+ self.obj = credentials.Credentials(**KWARGS)
2849+ self.assertEqual(self.obj.success_cb, NO_OP)
2850+ self.assertEqual(self.obj.error_cb, NO_OP)
2851+ self.assertEqual(self.obj.denial_cb, NO_OP)
2852+
2853+ def test_creation_parameters_are_stored(self):
2854+ """Creation parameters are stored."""
2855+ for key, value in KWARGS.iteritems():
2856+ self.assertEqual(getattr(self.obj, key), value)
2857+
2858+ def test_tc_url_defaults_to_none(self):
2859+ """The T&C url defaults to None."""
2860+ newkw = KWARGS.copy()
2861+ newkw.pop(TC_URL_KEY)
2862+ self.obj = credentials.Credentials(**newkw)
2863+
2864+ self.assertEqual(getattr(self.obj, TC_URL_KEY), None)
2865+
2866+ def test_help_text_defaults_to_empty_string(self):
2867+ """The T&C url defaults to the emtpy string."""
2868+ newkw = KWARGS.copy()
2869+ newkw.pop(HELP_TEXT_KEY)
2870+ self.obj = credentials.Credentials(**newkw)
2871+
2872+ self.assertEqual(getattr(self.obj, HELP_TEXT_KEY), '')
2873+
2874+ def test_window_id_defaults_to_zero(self):
2875+ """The T&C url defaults to 0."""
2876+ newkw = KWARGS.copy()
2877+ newkw.pop(WINDOW_ID_KEY)
2878+ self.obj = credentials.Credentials(**newkw)
2879+
2880+ self.assertEqual(getattr(self.obj, WINDOW_ID_KEY), 0)
2881+
2882+ def test_ping_url_defaults_to_none(self):
2883+ """The ping url defaults to None."""
2884+ newkw = KWARGS.copy()
2885+ newkw.pop(PING_URL_KEY)
2886+ self.obj = credentials.Credentials(**newkw)
2887+
2888+ self.assertEqual(getattr(self.obj, PING_URL_KEY), None)
2889+
2890+ def test_login_success_cb_if_cred_not_found(self):
2891+ """On auth success, if cred not found, self.error_cb is called."""
2892+ self.patch(credentials, 'keyring_get_credentials', lambda app: None)
2893+
2894+ self.obj._login_success_cb(dialog=None, app_name=APP_NAME, email=EMAIL)
2895+
2896+ msg = 'Creds are empty! This should not happen'
2897+ self.assert_error_cb_called(msg='Problem while retrieving credentials',
2898+ detailed_error=AssertionError(msg))
2899+
2900+ def test_login_success_cb_if_cred_error(self):
2901+ """On auth success, if cred failed, self.error_cb is called."""
2902+ self.patch(credentials, 'keyring_get_credentials', self.fail)
2903+
2904+ self.obj._login_success_cb(dialog=None, app_name=APP_NAME, email=EMAIL)
2905+
2906+ msg = 'Problem while retrieving credentials'
2907+ self.assert_error_cb_called(msg=msg, detailed_error=FailTest(APP_NAME))
2908+ self.assertTrue(self.memento.check_exception(FailTest, APP_NAME))
2909+
2910+ def test_login_success_cb_if_ping_success(self):
2911+ """Auth success + cred found + ping success, success_cb is called."""
2912+ self.patch(credentials, 'keyring_get_credentials', lambda app: TOKEN)
2913+ self.patch(self.obj, '_ping_url', lambda *a, **kw: 200)
2914+
2915+ self.obj._login_success_cb(dialog=None, app_name=APP_NAME, email=EMAIL)
2916+
2917+ self.assertEqual(self._called, (('success', APP_NAME, TOKEN), {}))
2918+
2919+ def test_login_success_cb_if_ping_error(self):
2920+ """Auth success + cred found + ping error, error_cb is called.
2921+
2922+ Credentials are removed. The exception is logged.
2923+ """
2924+ self.patch(credentials, 'keyring_get_credentials', lambda app: TOKEN)
2925+ error = 'Bla'
2926+ self.patch(credentials.urllib2, 'urlopen',
2927+ lambda *a, **kw: self.fail(error))
2928+ self._cred_cleared = False
2929+ self.patch(self.obj, 'clear_credentials',
2930+ lambda: setattr(self, '_cred_cleared', True))
2931+
2932+ self.obj._login_success_cb(dialog=None, app_name=APP_NAME, email=EMAIL)
2933+
2934+ # error cb called correctly
2935+ msg = 'Problem opening the ping_url'
2936+ self.assert_error_cb_called(msg=msg, detailed_error=FailTest(error))
2937+
2938+ # credentials cleared
2939+ self.assertTrue(self._cred_cleared)
2940+
2941+ # exception logged
2942+ self.assertTrue(self.memento.check_exception(FailTest, error))
2943+
2944+ def test_login_success_cb_pings_url(self):
2945+ """On auth success, self.ping_url is opened."""
2946+ self.patch(credentials, 'keyring_get_credentials', lambda app: TOKEN)
2947+
2948+ self._url_pinged = False
2949+ self.patch(self.obj, '_ping_url',
2950+ lambda *a, **kw: setattr(self, '_url_pinged', (a, kw)))
2951+
2952+ self.obj._login_success_cb(dialog=None, app_name=APP_NAME, email=EMAIL)
2953+
2954+ self.assertEqual(self._url_pinged, ((APP_NAME, EMAIL, TOKEN), {}))
2955+
2956+ def test_login_error_cb(self):
2957+ """On login/register error, self.error_cb is called."""
2958+ self.obj._login_error_cb(dialog=None, app_name=APP_NAME, error='Bla')
2959+ self.assert_error_cb_called(msg='Bla')
2960+
2961+ def test_auth_denial_cb(self):
2962+ """On auth denied, self.denial_cb is called."""
2963+ self.obj._auth_denial_cb(dialog=None, app_name=APP_NAME)
2964+ self.assertEqual(self._called, (('denial', APP_NAME), {}))
2965+
2966+
2967+class PingUrlTestCase(CredentialsTestCase):
2968+ """Test suite for the URL pinging."""
2969+
2970+ def setUp(self):
2971+ super(PingUrlTestCase, self).setUp()
2972+ self._request = None
2973+
2974+ def faked_urlopen(request):
2975+ """Fake urlopener."""
2976+ self._request = request
2977+ response = urllib.addinfourl(fp=open('/dev/null'),
2978+ headers=request.headers,
2979+ url=request.get_full_url(),
2980+ code=200)
2981+ return response
2982+
2983+ self.patch(credentials.urllib2, 'urlopen', faked_urlopen)
2984+
2985+ def test_ping_url_if_url_is_none(self):
2986+ """self.ping_url is opened."""
2987+ self.patch(credentials.urllib2, 'urlopen', self.fail)
2988+ self.obj.ping_url = None
2989+ self.obj._ping_url(app_name=APP_NAME, email=EMAIL, credentials=TOKEN)
2990+ # no failure
2991+
2992+ def test_ping_url(self):
2993+ """On auth success, self.ping_url is opened."""
2994+ self.obj._ping_url(app_name=APP_NAME, email=EMAIL, credentials=TOKEN)
2995+
2996+ self.assertIsInstance(self._request, credentials.urllib2.Request)
2997+ self.assertEqual(self._request.get_full_url(),
2998+ self.obj.ping_url + EMAIL)
2999+
3000+ def test_request_is_signed_with_credentials(self):
3001+ """The http request to self.ping_url is signed with the credentials."""
3002+ result = self.obj._ping_url(APP_NAME, EMAIL, TOKEN)
3003+ headers = self._request.headers
3004+
3005+ self.assertIn('Authorization', headers)
3006+ oauth_stuff = headers['Authorization']
3007+
3008+ expected = 'oauth_consumer_key="xQ7xDAz", ' \
3009+ 'oauth_signature_method="HMAC-SHA1", oauth_version="1.0", ' \
3010+ 'oauth_token="GkInOfSMGwTXAUoVQwLUoPxElEEUdhsLVNTPhxHJDUIeHCPNEo' \
3011+ '", oauth_signature="'
3012+ self.assertIn(expected, oauth_stuff)
3013+ self.assertEqual(result, 200)
3014+
3015+ def test_ping_url_error(self):
3016+ """Exception is handled if ping fails."""
3017+ error = 'Blu'
3018+ self.patch(credentials.urllib2, 'urlopen', lambda r: self.fail(error))
3019+
3020+ self.obj._ping_url(APP_NAME, EMAIL, TOKEN)
3021+
3022+ msg = 'Problem opening the ping_url'
3023+ self.assert_error_cb_called(msg=msg, detailed_error=FailTest(error))
3024+ self.assertTrue(self.memento.check_exception(FailTest, error))
3025+
3026+
3027+class FindCredentialsTestCase(CredentialsTestCase):
3028+ """Test suite for the find_credentials method."""
3029+
3030+ def test_find_credentials(self):
3031+ """The credentials are immediately returned if found."""
3032+ self.patch(credentials, 'keyring_get_credentials', lambda app: TOKEN)
3033+
3034+ token = self.obj.find_credentials()
3035+ self.assertEqual(token, TOKEN)
3036+
3037+ def test_credentials_not_found(self):
3038+ """find_credentials immediately returns '' when no token found."""
3039+ self.patch(credentials, 'keyring_get_credentials', lambda app: None)
3040+
3041+ token = self.obj.find_credentials()
3042+ self.assertEqual(token, {})
3043+
3044+ def test_keyring_failure(self):
3045+ """Failures from the keyring are handled."""
3046+ self.patch(credentials, 'keyring_get_credentials', self.fail)
3047+
3048+ self.obj.find_credentials()
3049+
3050+ msg = 'Problem while retrieving credentials'
3051+ self.assert_error_cb_called(msg=msg, detailed_error=FailTest(APP_NAME))
3052+ self.assertTrue(self.memento.check_exception(FailTest, APP_NAME))
3053+
3054+
3055+class ClearCredentialsTestCase(CredentialsTestCase):
3056+ """Test suite for the clear_credentials method."""
3057+
3058+ def test_clear_credentials(self):
3059+ """The credentials are cleared."""
3060+ self.patch(credentials, 'keyring_delete_credentials', self._set_called)
3061+
3062+ self.obj.clear_credentials()
3063+ self.assertEqual(self._called, ((APP_NAME,), {}))
3064+
3065+ def test_removed_from_keyring(self):
3066+ """The credentials are cleared from the keyring."""
3067+ self.patch(credentials.keyring.Keyring, 'delete_ubuntusso_attr',
3068+ self._set_called)
3069+
3070+ self.obj.clear_credentials()
3071+ self.assertEqual(self._called, ((), {}))
3072+
3073+ def test_keyring_failure(self):
3074+ """Failures from the keyring are handled."""
3075+ self.patch(credentials, 'keyring_delete_credentials', self.fail)
3076+
3077+ self.obj.clear_credentials()
3078+
3079+ msg = 'Problem while deleting credentials'
3080+ self.assert_error_cb_called(msg=msg, detailed_error=FailTest(APP_NAME))
3081+ self.assertTrue(self.memento.check_exception(FailTest, APP_NAME))
3082+
3083+
3084+class RegisterTestCase(CredentialsTestCase):
3085+ """Test suite for the register method."""
3086+
3087+ operation = 'register'
3088+ login_only = False
3089+
3090+ def test_with_existent_token(self):
3091+ """The operation returns the credentials if already in keyring."""
3092+ self.patch(credentials, 'keyring_get_credentials', lambda app: TOKEN)
3093+
3094+ getattr(self.obj, self.operation)()
3095+
3096+ self.assertEqual(self._called, (('success', APP_NAME, TOKEN), {}))
3097+
3098+ def test_without_existent_token(self):
3099+ """The operation returns the credentials gathered by the GUI."""
3100+ self.patch(credentials, 'keyring_get_credentials', lambda app: None)
3101+
3102+ getattr(self.obj, self.operation)()
3103+
3104+ expected = KWARGS.copy()
3105+ expected.pop(PING_URL_KEY)
3106+ expected['login_only'] = self.login_only
3107+ self.assertEqual(expected, self.kwargs)
3108+
3109+ def test_with_exception_on_credentials(self):
3110+ """The operation calls the error callback if a exception occurs."""
3111+ self.patch(credentials, 'keyring_get_credentials', self.fail)
3112+
3113+ getattr(self.obj, self.operation)()
3114+
3115+ msg = 'Problem while retrieving credentials'
3116+ self.assert_error_cb_called(msg=msg, detailed_error=FailTest(APP_NAME))
3117+ self.assertTrue(self.memento.check_exception(FailTest, APP_NAME))
3118+
3119+ def test_with_exception_on_gui(self):
3120+ """The operation calls the error callback if a GUI exception occurs."""
3121+ self.patch(credentials, 'keyring_get_credentials', lambda app: None)
3122+ err = 'Ble'
3123+ self.patch(gui, 'UbuntuSSOClientGUI', lambda *a, **kw: self.fail(err))
3124+
3125+ getattr(self.obj, self.operation)()
3126+
3127+ msg = 'Problem opening the Ubuntu SSO user interface'
3128+ self.assert_error_cb_called(msg=msg, detailed_error=FailTest(err))
3129+ self.assertTrue(self.memento.check_exception(FailTest, err))
3130+
3131+ def test_connects_gui_signals(self):
3132+ """Outcome signals from GUI are connected to callbacks."""
3133+ self.patch(credentials, 'keyring_get_credentials', lambda app: None)
3134+
3135+ expected = [
3136+ (gui.SIG_LOGIN_SUCCEEDED, self.obj._login_success_cb),
3137+ (gui.SIG_LOGIN_FAILED, self.obj._login_error_cb),
3138+ (gui.SIG_REGISTRATION_SUCCEEDED, self.obj._login_success_cb),
3139+ (gui.SIG_REGISTRATION_FAILED, self.obj._login_error_cb),
3140+ (gui.SIG_USER_CANCELATION, self.obj._auth_denial_cb),
3141+ ]
3142+ getattr(self.obj, self.operation)()
3143+ self.assertEqual(self.signals, expected)
3144+
3145+
3146+class LoginTestCase(RegisterTestCase):
3147+ """Test suite for the login method."""
3148+
3149+ operation = 'login'
3150+ login_only = True
3151
3152=== modified file 'ubuntu_sso/tests/test_gui.py'
3153--- ubuntu_sso/tests/test_gui.py 2010-09-08 19:25:02 +0000
3154+++ ubuntu_sso/tests/test_gui.py 2010-10-18 13:21:21 +0000
3155@@ -23,6 +23,8 @@
3156 import logging
3157 import os
3158
3159+from collections import defaultdict
3160+
3161 import dbus
3162 import gtk
3163 import webkit
3164@@ -31,31 +33,17 @@
3165
3166 from contrib.testing.testcase import MementoHandler
3167 from ubuntu_sso import gui
3168+from ubuntu_sso.tests import (APP_NAME, TC_URL, HELP_TEXT, CAPTCHA_ID,
3169+ CAPTCHA_SOLUTION, EMAIL, EMAIL_TOKEN, NAME, PASSWORD, RESET_PASSWORD_TOKEN)
3170
3171
3172 # Access to a protected member 'yyy' of a client class
3173 # pylint: disable=W0212
3174+
3175 # Instance of 'UbuntuSSOClientGUI' has no 'yyy' member
3176 # pylint: disable=E1101,E1103
3177
3178
3179-APP_NAME = 'The Super testing app!'
3180-TC_URI = 'http://localhost'
3181-HELP_TEXT = """Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam sed
3182-lorem nibh. Suspendisse gravida nulla non nunc suscipit pulvinar tempus ut
3183-augue. Morbi consequat, ligula a elementum pretium, dolor nulla tempus metus,
3184-sed viverra nisi risus non velit."""
3185-
3186-CAPTCHA_ID = 'test'
3187-CAPTCHA_SOLUTION = 'william Byrd'
3188-EMAIL = 'test@example.com'
3189-EMAIL_TOKEN = 'B2Pgtf'
3190-NAME = 'Juanito Pérez'
3191-PASSWORD = 'h3lloWorld'
3192-RESET_PASSWORD_TOKEN = '8G5Wtq'
3193-TOKEN_NAME = 'my-testing-host'
3194-
3195-
3196 class FakedSSOBackend(object):
3197 """Fake a SSO Backend (acts as a dbus.Interface as well)."""
3198
3199@@ -107,7 +95,8 @@
3200 def get_object(self, bus_name, object_path, introspect=True,
3201 follow_name_owner_changes=False, **kwargs):
3202 """Return a faked proxy for the given remote object."""
3203- if bus_name == gui.DBUS_BUS_NAME and object_path == gui.DBUS_PATH:
3204+ if bus_name == gui.DBUS_BUS_NAME and \
3205+ object_path == gui.DBUS_ACCOUNT_PATH:
3206 assert self.obj is None
3207 kwargs = dict(object_path=object_path,
3208 bus_name=bus_name, follow_name_owner_changes=True)
3209@@ -115,6 +104,56 @@
3210 return self.obj
3211
3212
3213+class Settings(dict):
3214+ """Faked embedded browser settings."""
3215+
3216+ def get_property(self, prop_name):
3217+ """Alias for __getitem__."""
3218+ return self[prop_name]
3219+
3220+ def set_property(self, prop_name, newval):
3221+ """Alias for __setitem__."""
3222+ self[prop_name] = newval
3223+
3224+
3225+class FakedEmbeddedBrowser(gtk.TextView):
3226+ """Faked an embedded browser."""
3227+
3228+ def __init__(self):
3229+ super(FakedEmbeddedBrowser, self).__init__()
3230+ self._props = {}
3231+ self._signals = defaultdict(list)
3232+ self._settings = Settings()
3233+
3234+ def connect(self, signal_name, callback):
3235+ """Connect 'signal_name' with 'callback'."""
3236+ self._signals[signal_name].append(callback)
3237+
3238+ def load_uri(self, uri):
3239+ """Navigate to the given 'uri'."""
3240+ self._props['uri'] = uri
3241+
3242+ def set_property(self, prop_name, newval):
3243+ """Set 'prop_name' to 'newval'."""
3244+ self._props[prop_name] = newval
3245+
3246+ def get_property(self, prop_name):
3247+ """Return the current value for 'prop_name'."""
3248+ return self._props[prop_name]
3249+
3250+ def get_settings(self,):
3251+ """Return the current settings."""
3252+ return self._settings
3253+
3254+ def get_load_status(self):
3255+ """Return the current load status."""
3256+ return gui.WEBKIT_LOAD_FINISHED
3257+
3258+ def show(self):
3259+ """Show this instance."""
3260+ self.set_property('visible', True)
3261+
3262+
3263 class BasicTestCase(TestCase):
3264 """Test case with a helper tracker."""
3265
3266@@ -340,7 +379,7 @@
3267 """Basic setup and helper functions."""
3268
3269 gui_class = gui.UbuntuSSOClientGUI
3270- kwargs = dict(app_name=APP_NAME, tc_uri=TC_URI, help_text=HELP_TEXT)
3271+ kwargs = dict(app_name=APP_NAME, tc_url=TC_URL, help_text=HELP_TEXT)
3272
3273 def setUp(self):
3274 """Init."""
3275@@ -498,7 +537,7 @@
3276 """SessionBus.get_object is called properly."""
3277 self.assertIsInstance(self.ui.bus.obj, FakedDbusObject)
3278 self.assertEqual(self.ui.bus.obj._args, ())
3279- expected = dict(object_path=gui.DBUS_PATH,
3280+ expected = dict(object_path=gui.DBUS_ACCOUNT_PATH,
3281 bus_name=gui.DBUS_BUS_NAME,
3282 follow_name_owner_changes=True)
3283 self.assertEqual(expected, self.ui.bus.obj._kwargs)
3284@@ -549,6 +588,13 @@
3285 # text content is correct
3286 self.assertEqual(expected, actual, msg % (name, expected, actual))
3287
3288+ def test_entries_activates_default(self):
3289+ """Entries have the activates default prop set."""
3290+ msg = '"%s" must have activates_default set to True.'
3291+ for name in self.ui.entries:
3292+ entry = getattr(self.ui, name)
3293+ self.assertTrue(entry.get_activates_default(), msg % (name,))
3294+
3295 def test_initial_size_for_labels(self):
3296 """Labels have the correct width."""
3297 expected = (int(self.ui.window.get_size_request()[0] * 0.9), -1)
3298@@ -585,17 +631,6 @@
3299 self.assertEqual(self._called, ((widget,), {}), msg % name)
3300 self._called = False
3301
3302- def test_all_buttons_are_activatable(self):
3303- """Every button should be activatable."""
3304- msg = '"%s" should be activatable.'
3305- buttons = filter(lambda name: '_button' in name, self.ui.widgets)
3306- for name in buttons:
3307- widget = getattr(self.ui, name)
3308- widget.connect('clicked', self._set_called)
3309- widget.activate()
3310- self.assertEqual(self._called, ((widget,), {}), msg % name)
3311- self._called = False
3312-
3313 def test_window_icon(self):
3314 """Main window has the proper icon."""
3315 self.assertEqual('ubuntu-logo', self.ui.window.get_icon_name())
3316@@ -790,6 +825,13 @@
3317 self.assertTrue(self.ui.captcha_image.get_property('visible'))
3318 self.assertEqual(self._called, ((self.ui._captcha_filename,), {}))
3319
3320+ def test_on_captcha_generated_logs_captcha_id_when_none(self):
3321+ """If the captcha id is None, a warning is logged."""
3322+ self.ui.on_captcha_generated(app_name=APP_NAME, captcha_id=None)
3323+ self.assertTrue(self.memento.check(logging.WARNING, APP_NAME))
3324+ self.assertTrue(self.memento.check(logging.WARNING,
3325+ 'captcha_id is None'))
3326+
3327 def test_captcha_reload_button_visible(self):
3328 """The captcha reload button is initially visible."""
3329 self.assertTrue(self.ui.captcha_reload_button.get_visible(),
3330@@ -812,38 +854,6 @@
3331 actual = self.ui.login_button.get_label()
3332 self.assertEqual(self.ui.LOGIN_BUTTON_LABEL, actual)
3333
3334- def test_activate_name_entry_clicks_connect(self):
3335- """Activating any entry generates a connect attempt."""
3336- self.ui.join_ok_button.connect('clicked', self._set_called)
3337- self.ui.name_entry.activate()
3338- self.assertEqual(self._called, ((self.ui.join_ok_button,), {}))
3339-
3340- def test_activate_email_entry_clicks_connect(self):
3341- """Activating any entry generates a connect attempt."""
3342- self.ui.join_ok_button.connect('clicked', self._set_called)
3343- self.ui.email1_entry.activate()
3344- self.assertEqual(self._called, ((self.ui.join_ok_button,), {}))
3345-
3346- self._called = False
3347- self.ui.email2_entry.activate()
3348- self.assertEqual(self._called, ((self.ui.join_ok_button,), {}))
3349-
3350- def test_activate_password_entry_clicks_connect(self):
3351- """Activating any entry generates a connect attempt."""
3352- self.ui.join_ok_button.connect('clicked', self._set_called)
3353- self.ui.password1_entry.activate()
3354- self.assertEqual(self._called, ((self.ui.join_ok_button,), {}))
3355-
3356- self._called = False
3357- self.ui.password2_entry.activate()
3358- self.assertEqual(self._called, ((self.ui.join_ok_button,), {}))
3359-
3360- def test_activate_captcha_solution_entry_clicks_connect(self):
3361- """Activating any entry generates a connect attempt."""
3362- self.ui.join_ok_button.connect('clicked', self._set_called)
3363- self.ui.captcha_solution_entry.activate()
3364- self.assertEqual(self._called, ((self.ui.join_ok_button,), {}))
3365-
3366 def test_join_ok_button_does_nothing_if_clicked_but_disabled(self):
3367 """The join form can only be submitted if the button is sensitive."""
3368 self.patch(self.ui.email1_entry, 'get_text', self._set_called)
3369@@ -860,11 +870,11 @@
3370 class NoTermsAndConditionsTestCase(UbuntuSSOClientTestCase):
3371 """Test suite for the user registration (with no t&c link)."""
3372
3373- kwargs = dict(app_name=APP_NAME, tc_uri='', help_text=HELP_TEXT)
3374+ kwargs = dict(app_name=APP_NAME, tc_url='', help_text=HELP_TEXT)
3375
3376 def test_no_tc_link(self):
3377 """The T&C button and checkbox are not shown if no link is provided"""
3378- self.assertEqual(self.ui.tc_hbox.get_visible(), False)
3379+ self.assertEqual(self.ui.tc_vbox.get_visible(), False)
3380
3381
3382 class TermsAndConditionsTestCase(UbuntuSSOClientTestCase):
3383@@ -875,61 +885,160 @@
3384 self.assertEqual(self.ui.tc_button.get_visible(), True)
3385 self.assertEqual(self.ui.yes_to_tc_checkbutton.get_visible(), True)
3386
3387+
3388+class TermsAndConditionsBrowserTestCase(UbuntuSSOClientTestCase):
3389+ """Test suite for the terms & conditions browser."""
3390+
3391+ def setUp(self):
3392+ super(TermsAndConditionsBrowserTestCase, self).setUp()
3393+ self.patch(webkit, 'WebView', FakedEmbeddedBrowser)
3394+
3395+ self.ui.tc_button.clicked()
3396+ children = self.ui.tc_browser_window.get_children()
3397+ assert len(children) == 1
3398+ self.browser = children[0]
3399+
3400+ def tearDown(self):
3401+ self.ui.tc_browser_vbox.hide()
3402+ super(TermsAndConditionsBrowserTestCase, self).tearDown()
3403+
3404 def test_tc_browser_is_created_when_tc_page_is_shown(self):
3405- """The webkit browser is created when the TC page is shown."""
3406- self.ui.tc_browser_vbox.show()
3407+ """The browser is created when the TC button is clicked."""
3408+ self.ui.on_tc_browser_notify_load_status(self.browser)
3409
3410 children = self.ui.tc_browser_window.get_children()
3411 self.assertEqual(1, len(children))
3412- browser = children[0]
3413- self.assertIsInstance(browser, webkit.WebView)
3414- self.assertTrue(browser.get_property('visible'))
3415-
3416- settings = browser.get_settings()
3417+
3418+ def test_is_visible(self):
3419+ """The browser is visible."""
3420+ self.assertIsInstance(self.browser, FakedEmbeddedBrowser)
3421+ self.assertTrue(self.browser.get_property('visible'))
3422+
3423+ def test_settings(self):
3424+ """The browser settings are correct."""
3425+ settings = self.browser.get_settings()
3426 self.assertFalse(settings.get_property('enable-plugins'))
3427 self.assertFalse(settings.get_property('enable-default-context-menu'))
3428
3429- self.ui.tc_browser_vbox.hide()
3430-
3431 def test_tc_browser_is_destroyed_when_tc_page_is_hid(self):
3432- """The webkit browser is destroyed when the TC page is hid."""
3433- self.ui.tc_browser_vbox.show()
3434- browser = self.ui.tc_browser_window.get_children()[0]
3435- self.patch(browser, 'destroy', self._set_called)
3436+ """The browser is destroyed when the TC page is hid."""
3437+ self.ui.on_tc_browser_notify_load_status(self.browser)
3438+ self.patch(self.browser, 'destroy', self._set_called)
3439+ self.ui.tc_browser_vbox.hide()
3440+ self.assertEqual(self._called, ((), {}))
3441+
3442+ def test_tc_browser_is_removed_when_tc_page_is_hid(self):
3443+ """The browser is removed when the TC page is hid."""
3444+ self.ui.on_tc_browser_notify_load_status(self.browser)
3445+
3446 self.ui.tc_browser_vbox.hide()
3447
3448 children = self.ui.tc_browser_window.get_children()
3449 self.assertEqual(0, len(children))
3450- self.assertEqual(self._called, ((), {}))
3451
3452- def test_tc_button_clicked_morphs_into_tc_browser_vbox(self):
3453- """Terms & Conditions morphs to a browser window."""
3454- self.ui.tc_button.clicked()
3455- self.assert_pages_visibility(tc_browser=True)
3456+ def test_tc_button_clicked_morphs_into_processing_page(self):
3457+ """Clicking the T&C button morphs into processing page."""
3458+ self.assert_pages_visibility(processing=True)
3459
3460 def test_tc_back_clicked_returns_to_previous_page(self):
3461 """Terms & Conditions back button return to previous page."""
3462- self.ui.tc_button.clicked()
3463+ self.ui.on_tc_browser_notify_load_status(self.browser)
3464 self.ui.tc_back_button.clicked()
3465 self.assert_pages_visibility(enter_details=True)
3466
3467 def test_tc_button_has_the_proper_wording(self):
3468 """Terms & Conditions has the proper wording."""
3469- self.assertEqual(self.ui.tc_button.get_label(), self.ui.TC)
3470+ self.assertEqual(self.ui.tc_button.get_label(), self.ui.TC_BUTTON)
3471
3472 def test_tc_has_no_help_text(self):
3473 """The help text is removed."""
3474- self.ui.tc_button.clicked()
3475+ self.ui.on_tc_browser_notify_load_status(self.browser)
3476 self.assertEqual('', self.ui.help_label.get_text())
3477
3478- def test_tc_browser_opens_the_proper_uri(self):
3479+ def test_tc_browser_opens_the_proper_url(self):
3480 """Terms & Conditions browser shows the proper uri."""
3481- self.ui.tc_button.clicked()
3482- self.assertEqual(self.ui.tc_browser.get_property('uri'), TC_URI)
3483+ self.assertEqual(self.browser.get_property('uri'), TC_URL)
3484+
3485+ def test_notify_load_status_connected(self):
3486+ """The 'notify::load-status' signal is connected."""
3487+ expected = [self.ui.on_tc_browser_notify_load_status]
3488+ self.assertEqual(self.browser._signals['notify::load-status'],
3489+ expected)
3490
3491 # Unused variable 'skip'
3492 # pylint: disable=W0612
3493- test_tc_browser_opens_the_proper_uri.skip = 'The freaking test wont work.'
3494+ test_notify_load_status_connected.skip = \
3495+ 'Connecting to notify::load-status makes U1 terms navigation fail.'
3496+
3497+ def test_notify_load_finished_connected(self):
3498+ """The 'load-finished' signal is connected."""
3499+ expected = [self.ui.on_tc_browser_notify_load_status]
3500+ self.assertEqual(self.browser._signals['load-finished'],
3501+ expected)
3502+
3503+ def test_tc_loaded_morphs_into_tc_browser_vbox(self):
3504+ """When the Terms & Conditions is loaded, show the browser window."""
3505+ self.ui.on_tc_browser_notify_load_status(self.browser)
3506+ self.assert_pages_visibility(tc_browser=True)
3507+
3508+ def test_navigation_requested_connected(self):
3509+ """The 'navigation-policy-decision-requested' signal is connected."""
3510+ actual = self.browser._signals['navigation-policy-decision-requested']
3511+ expected = [self.ui.on_tc_browser_navigation_requested]
3512+ self.assertEqual(actual, expected)
3513+
3514+ def test_navigation_requested_succeeds_for_no_clicking(self):
3515+ """The navigation request succeeds when user hasn't clicked a link."""
3516+ action = webkit.WebNavigationAction()
3517+ action.set_reason(gui.WEBKIT_WEB_NAVIGATION_REASON_OTHER)
3518+
3519+ decision = webkit.WebPolicyDecision()
3520+ decision.use = self._set_called
3521+
3522+ kwargs = dict(browser=self.browser, frame=None, request=None,
3523+ action=action, decision=decision)
3524+ self.ui.on_tc_browser_navigation_requested(**kwargs)
3525+ self.assertEqual(self._called, ((), {}))
3526+
3527+ def test_navigation_requested_ignores_clicked_links(self):
3528+ """The navigation request is ignored if a link was clicked."""
3529+ action = webkit.WebNavigationAction()
3530+ action.set_reason(gui.WEBKIT_WEB_NAVIGATION_REASON_LINK_CLICKED)
3531+
3532+ decision = webkit.WebPolicyDecision()
3533+ decision.ignore = self._set_called
3534+
3535+ kwargs = dict(browser=self.browser, frame=None, request=None,
3536+ action=action, decision=decision)
3537+ self.ui.on_tc_browser_navigation_requested(**kwargs)
3538+ self.assertEqual(self._called, ((), {}))
3539+
3540+ def test_navigation_requested_ignores_for_none(self):
3541+ """The navigation request is ignoref the request if params are None."""
3542+ kwargs = dict(browser=None, frame=None, request=None,
3543+ action=None, decision=None)
3544+ self.ui.on_tc_browser_navigation_requested(**kwargs)
3545+
3546+ def test_navigation_requested_opens_links_when_clicked(self):
3547+ """The navigation request is opened on user's default browser
3548+
3549+ (If the user opened a link by clicking into it).
3550+
3551+ """
3552+ url = 'http://something.com/yadda'
3553+ action = webkit.WebNavigationAction()
3554+ action.set_reason(gui.WEBKIT_WEB_NAVIGATION_REASON_LINK_CLICKED)
3555+ action.set_original_uri(url)
3556+
3557+ decision = webkit.WebPolicyDecision()
3558+ decision.ignore = gui.NO_OP
3559+
3560+ self.patch(gui.webbrowser, 'open', self._set_called)
3561+
3562+ kwargs = dict(browser=self.browser, frame=None, request=None,
3563+ action=action, decision=decision)
3564+ self.ui.on_tc_browser_navigation_requested(**kwargs)
3565+ self.assertEqual(self._called, ((url,), {}))
3566
3567
3568 class RegistrationErrorTestCase(UbuntuSSOClientTestCase):
3569@@ -1059,12 +1168,6 @@
3570 self.ui.success_close_button.clicked()
3571 self.assertFalse(self.ui.window.get_property('visible'))
3572
3573- def test_activate_email_token_entry_clicks_verify_token(self):
3574- """Activating any entry generates a connect attempt."""
3575- self.ui.verify_token_button.connect('clicked', self._set_called)
3576- self.ui.email_token_entry.activate()
3577- self.assertEqual(self._called, ((self.ui.verify_token_button,), {}))
3578-
3579 def test_verify_token_button_does_nothing_if_clicked_but_disabled(self):
3580 """The email token can only be submitted if the button is sensitive."""
3581 self.patch(self.ui.email_token_entry, 'get_text', self._set_called)
3582@@ -1353,18 +1456,6 @@
3583 self.ui.login_back_button.clicked()
3584 self.assertFalse(self.ui.warning_label.get_property('visible'))
3585
3586- def test_activate_email_entry_clicks_login(self):
3587- """Activating any entry generates a connect attempt."""
3588- self.ui.login_ok_button.connect('clicked', self._set_called)
3589- self.ui.login_email_entry.activate()
3590- self.assertEqual(self._called, ((self.ui.login_ok_button,), {}))
3591-
3592- def test_activate_password_entry_clicks_login(self):
3593- """Activating any entry generates a connect attempt."""
3594- self.ui.login_ok_button.connect('clicked', self._set_called)
3595- self.ui.login_password_entry.activate()
3596- self.assertEqual(self._called, ((self.ui.login_ok_button,), {}))
3597-
3598 def test_login_ok_button_does_nothing_if_clicked_but_disabled(self):
3599 """The join form can only be submitted if the button is sensitive."""
3600 self.patch(self.ui.login_email_entry, 'get_text', self._set_called)
3601@@ -1541,14 +1632,6 @@
3602 expected = '\n'.join((error['__all__'], error['message']))
3603 self.assert_correct_label_warning(self.ui.warning_label, expected)
3604
3605- def test_activate_reset_email_entry_clicks_login(self):
3606- """Activating any entry generates a connect attempt."""
3607- self.ui.request_password_token_ok_button.connect('clicked',
3608- self._set_called)
3609- self.ui.reset_email_entry.activate()
3610- self.assertEqual(self._called,
3611- ((self.ui.request_password_token_ok_button,), {}))
3612-
3613 def test_ok_button_does_nothing_if_clicked_but_disabled(self):
3614 """The password token can be requested if the button is sensitive."""
3615 self.patch(self.ui.reset_email_entry, 'get_text', self._set_called)
3616@@ -1673,25 +1756,6 @@
3617 expected = '\n'.join((error['__all__'], error['message']))
3618 self.assert_correct_label_warning(self.ui.warning_label, expected)
3619
3620- def test_activate_reset_code_entry_clicks_set_new_password(self):
3621- """Activating any entry generates a connect attempt."""
3622- self.ui.set_new_password_ok_button.connect('clicked', self._set_called)
3623- self.ui.reset_code_entry.activate()
3624- self.assertEqual(self._called,
3625- ((self.ui.set_new_password_ok_button,), {}))
3626-
3627- def test_activate_password_entry_clicks_set_new_password(self):
3628- """Activating any entry generates a connect attempt."""
3629- self.ui.set_new_password_ok_button.connect('clicked', self._set_called)
3630- self.ui.reset_password1_entry.activate()
3631- self.assertEqual(self._called,
3632- ((self.ui.set_new_password_ok_button,), {}))
3633-
3634- self._called = False
3635- self.ui.reset_password2_entry.activate()
3636- self.assertEqual(self._called,
3637- ((self.ui.set_new_password_ok_button,), {}))
3638-
3639 def test_ok_button_does_nothing_if_clicked_but_disabled(self):
3640 """The new passwrd can only be set if the button is sensitive."""
3641 self.patch(self.ui.reset_code_entry, 'get_text', self._set_called)
3642@@ -1896,7 +1960,7 @@
3643 class LoginOnlyTestCase(UbuntuSSOClientTestCase):
3644 """Test suite for the login only GUI."""
3645
3646- kwargs = dict(app_name=APP_NAME, tc_uri=None, help_text=HELP_TEXT,
3647+ kwargs = dict(app_name=APP_NAME, tc_url=None, help_text=HELP_TEXT,
3648 login_only=True)
3649
3650 def test_login_is_first_page(self):
3651@@ -1922,6 +1986,8 @@
3652 def setUp(self):
3653 """Init."""
3654 super(SignalsTestCase, self).setUp()
3655+ self._expected_error = (APP_NAME, '\n'.join('%s: %s' % i
3656+ for i in self.error.iteritems()))
3657 self._called = {}
3658 for sig_name, _ in gui.SIGNAL_ARGUMENTS:
3659 self.ui.connect(sig_name, self._set_called, sig_name)
3660@@ -1955,7 +2021,7 @@
3661 """On UserRegistrationError, 'registration-failed' signal is sent."""
3662 self.ui.on_user_registration_error(app_name=APP_NAME, error=self.error)
3663 self.ui.on_close_clicked()
3664- expected = (self.ui.window, (APP_NAME, self.ui.UNKNOWN_ERROR), {})
3665+ expected = (self.ui.window, self._expected_error, {})
3666 self.assertEqual(expected,
3667 self._called[gui.SIG_REGISTRATION_FAILED])
3668
3669@@ -1970,7 +2036,7 @@
3670 """On EmailValidationError, 'registration-failed' signal is sent."""
3671 self.ui.on_email_validation_error(app_name=APP_NAME, error=self.error)
3672 self.ui.on_close_clicked()
3673- expected = (self.ui.window, (APP_NAME, self.ui.UNKNOWN_ERROR), {})
3674+ expected = (self.ui.window, self._expected_error, {})
3675 self.assertEqual(expected,
3676 self._called[gui.SIG_REGISTRATION_FAILED])
3677
3678@@ -1986,7 +2052,7 @@
3679 self.click_connect_with_valid_data()
3680 self.ui.on_login_error(app_name=APP_NAME, error=self.error)
3681 self.ui.on_close_clicked()
3682- expected = (self.ui.window, (APP_NAME, self.ui.UNKNOWN_ERROR), {})
3683+ expected = (self.ui.window, self._expected_error, {})
3684 self.assertEqual(expected,
3685 self._called[gui.SIG_LOGIN_FAILED])
3686
3687
3688=== modified file 'ubuntu_sso/tests/test_keyring.py'
3689--- ubuntu_sso/tests/test_keyring.py 2010-09-22 16:47:09 +0000
3690+++ ubuntu_sso/tests/test_keyring.py 2010-10-18 13:21:21 +0000
3691@@ -1,3 +1,5 @@
3692+# -*- coding: utf-8 -*-
3693+#
3694 # test_keyring - tests for ubuntu_sso.keyring
3695 #
3696 # Author: Alejandro J. Cura <alecu@canonical.com>
3697@@ -26,8 +28,7 @@
3698 from twisted.trial.unittest import TestCase
3699
3700 from ubuntu_sso import keyring
3701-
3702-APP_NAME = 'Yadda Yadda Doo'
3703+from ubuntu_sso.tests import APP_NAME
3704
3705
3706 def build_fake_gethostname(fake_hostname):
3707
3708=== modified file 'ubuntu_sso/tests/test_main.py'
3709--- ubuntu_sso/tests/test_main.py 2010-09-08 19:25:02 +0000
3710+++ ubuntu_sso/tests/test_main.py 2010-10-18 13:21:21 +0000
3711@@ -1,3 +1,5 @@
3712+# -*- coding: utf-8 -*-
3713+#
3714 # test_main - tests for ubuntu_sso.main
3715 #
3716 # Author: Natalia Bidart <natalia.bidart@canonical.com>
3717@@ -20,15 +22,8 @@
3718
3719 import logging
3720 import os
3721-import urllib2
3722-
3723-import gobject
3724-
3725-# Unable to import 'lazr.restfulclient.*'
3726-# pylint: disable=F0401
3727-from lazr.restfulclient.errors import HTTPError
3728-# pylint: enable=F0401
3729-from mocker import Mocker, MockerTestCase, ARGS, KWARGS, ANY
3730+
3731+from mocker import Mocker, MockerTestCase, ARGS, KWARGS
3732 from twisted.internet.defer import Deferred
3733 from twisted.trial.unittest import TestCase
3734
3735@@ -36,342 +31,45 @@
3736 import ubuntu_sso.main
3737
3738 from contrib.testing.testcase import MementoHandler
3739-from ubuntu_sso import gui
3740-from ubuntu_sso.keyring import get_token_name, U1_APP_NAME
3741-from ubuntu_sso.main import (
3742- AuthenticationError, blocking, EmailTokenError,
3743- except_to_errdict, InvalidEmailError, InvalidPasswordError,
3744- keyring_get_credentials, keyring_store_credentials, logger,
3745- NewPasswordError, PING_URL, SERVICE_URL,
3746- RegistrationError, ResetPasswordTokenError,
3747- SSOCredentials, SSOLogin, SSOLoginProcessor)
3748+from ubuntu_sso import credentials, DBUS_CREDENTIALS_IFACE
3749+from ubuntu_sso.keyring import U1_APP_NAME
3750+from ubuntu_sso.main import (U1_PING_URL, blocking, except_to_errdict,
3751+ CredentialsManagement, keyring_store_credentials, SSOCredentials, SSOLogin)
3752+from ubuntu_sso.main import (HELP_TEXT_KEY, PING_URL_KEY,
3753+ TC_URL_KEY, WINDOW_ID_KEY, SUCCESS_CB_KEY, ERROR_CB_KEY, DENIAL_CB_KEY)
3754+from ubuntu_sso.tests import (APP_NAME, TC_URL, HELP_TEXT, CAPTCHA_ID,
3755+ CAPTCHA_SOLUTION, EMAIL, EMAIL_TOKEN, PASSWORD, PING_URL, TOKEN,
3756+ TOKEN_NAME, WINDOW_ID)
3757
3758
3759 # Access to a protected member 'yyy' of a client class
3760 # pylint: disable=W0212
3761
3762
3763-APP_NAME = 'The Coolest App Ever'
3764-CAPTCHA_PATH = os.path.abspath(os.path.join(os.curdir, 'ubuntu_sso', 'tests',
3765- 'files', 'captcha.png'))
3766-CAPTCHA_ID = 'test'
3767-CAPTCHA_SOLUTION = 'william Byrd'
3768-CANT_RESET_PASSWORD_CONTENT = "CanNotResetPassowrdError: " \
3769- "Can't reset password for this account"
3770-RESET_TOKEN_INVALID_CONTENT = "AuthToken matching query does not exist."
3771-EMAIL = 'test@example.com'
3772-EMAIL_ALREADY_REGISTERED = 'a@example.com'
3773-EMAIL_TOKEN = 'B2Pgtf'
3774-HELP = 'help text'
3775-PASSWORD = 'be4tiFul'
3776-RESET_PASSWORD_TOKEN = '8G5Wtq'
3777-TOKEN = {u'consumer_key': u'xQ7xDAz',
3778- u'consumer_secret': u'KzCJWCTNbbntwfyCKKjomJDzlgqxLy',
3779- u'token_name': u'test',
3780- u'token': u'GkInOfSMGwTXAUoVQwLUoPxElEEUdhsLVNTPhxHJDUIeHCPNEo',
3781- u'token_secret': u'qFYImEtlczPbsCnYyuwLoPDlPEnvNcIktZphPQklAWrvyfFMV'}
3782-TOKEN_NAME = get_token_name(APP_NAME)
3783-STATUS_UNKNOWN = {'status': 'yadda-yadda'}
3784-STATUS_ERROR = {'status': 'error', 'errors': {'something': ['Bla', 'Ble']}}
3785-STATUS_OK = {'status': 'ok'}
3786-STATUS_EMAIL_UNKNOWN = {'status': 'yadda-yadda'}
3787-STATUS_EMAIL_ERROR = {'errors': {'email_token': ['Error1', 'Error2']}}
3788-STATUS_EMAIL_OK = {'email': EMAIL}
3789-TC_URL = 'tcurl'
3790-WINDOW_ID = 5
3791-
3792-NO_OP = lambda *args, **kwargs: None
3793-LOGIN_OR_REGISTER_ARGS = (APP_NAME, TC_URL, HELP, WINDOW_ID)
3794-LOGIN_OR_REGISTER_GUI_ARGS = LOGIN_OR_REGISTER_ARGS + (False,)
3795-LOGIN_ONLY_ARGS = (APP_NAME, HELP, WINDOW_ID)
3796-LOGIN_ONLY_GUI_ARGS = (APP_NAME, None, HELP, WINDOW_ID, True)
3797-
3798-
3799-class FakedResponse(object):
3800- """Fake a urlopen response."""
3801-
3802- def __init__(self, *args, **kwargs):
3803- for k, val in kwargs.iteritems():
3804- setattr(self, k, val)
3805-
3806-
3807-class FakedCaptchas(object):
3808- """Fake the captcha generator."""
3809-
3810- def new(self):
3811- """Return a fix captcha)."""
3812- return {'image_url': 'file://%s' % CAPTCHA_PATH,
3813- 'captcha_id': CAPTCHA_ID}
3814-
3815-
3816-class FakedRegistrations(object):
3817- """Fake the registrations service."""
3818-
3819- def register(self, email, password, captcha_id, captcha_solution):
3820- """Fake registration. Return a fix result."""
3821- if email == EMAIL_ALREADY_REGISTERED:
3822- return {'status': 'error',
3823- 'errors': {'email': 'Email already registered'}}
3824- elif captcha_id is None and captcha_solution is None:
3825- return STATUS_UNKNOWN
3826- elif captcha_id != CAPTCHA_ID or captcha_solution != CAPTCHA_SOLUTION:
3827- return STATUS_ERROR
3828- else:
3829- return STATUS_OK
3830-
3831- def request_password_reset_token(self, email):
3832- """Fake password reset token. Return a fix result."""
3833- if email is None:
3834- return STATUS_UNKNOWN
3835- elif email != EMAIL:
3836- raise HTTPError(response=None, content=CANT_RESET_PASSWORD_CONTENT)
3837- else:
3838- return STATUS_OK
3839-
3840- def set_new_password(self, email, token, new_password):
3841- """Fake the setting of new password. Return a fix result."""
3842- if email is None and token is None and new_password is None:
3843- return STATUS_UNKNOWN
3844- elif email != EMAIL or token != RESET_PASSWORD_TOKEN:
3845- raise HTTPError(response=None, content=RESET_TOKEN_INVALID_CONTENT)
3846- else:
3847- return STATUS_OK
3848-
3849-
3850-class FakedAuthentications(object):
3851- """Fake the authentications service."""
3852-
3853- def authenticate(self, token_name):
3854- """Fake authenticate. Return a fix result."""
3855- if not token_name.startswith(TOKEN_NAME):
3856- raise HTTPError(response=None, content=None)
3857- else:
3858- return TOKEN
3859-
3860-
3861-class FakedAccounts(object):
3862- """Fake the accounts service."""
3863-
3864- def validate_email(self, email_token):
3865- """Fake the email validation. Return a fix result."""
3866- if email_token is None:
3867- return STATUS_EMAIL_UNKNOWN
3868- elif email_token == EMAIL_ALREADY_REGISTERED:
3869- return {'status': 'error',
3870- 'errors': {'email': 'Email already registered'}}
3871- elif email_token != EMAIL_TOKEN:
3872- return STATUS_EMAIL_ERROR
3873- else:
3874- return STATUS_EMAIL_OK
3875-
3876-
3877-class FakedSSOServer(object):
3878- """Fake an SSO server."""
3879-
3880- def __init__(self, authorizer, service_root):
3881- self.captchas = FakedCaptchas()
3882- self.registrations = FakedRegistrations()
3883- self.authentications = FakedAuthentications()
3884- self.accounts = FakedAccounts()
3885-
3886-
3887-class SSOLoginProcessorTestCase(TestCase, MockerTestCase):
3888- """Test suite for the SSO login processor."""
3889-
3890- def setUp(self):
3891- """Init."""
3892- self.processor = SSOLoginProcessor(sso_service_class=FakedSSOServer)
3893- self.register_kwargs = dict(email=EMAIL, password=PASSWORD,
3894- captcha_id=CAPTCHA_ID,
3895- captcha_solution=CAPTCHA_SOLUTION)
3896- self.login_kwargs = dict(email=EMAIL, password=PASSWORD,
3897- token_name=TOKEN_NAME)
3898-
3899- def tearDown(self):
3900- """Clean up."""
3901- self.processor = None
3902-
3903- def test_generate_captcha(self):
3904- """Captcha can be generated."""
3905- filename = self.mktemp()
3906- self.addCleanup(lambda: os.remove(filename))
3907- captcha_id = self.processor.generate_captcha(filename)
3908- self.assertEqual(CAPTCHA_ID, captcha_id, 'captcha id must be correct.')
3909- self.assertTrue(os.path.isfile(filename), '%s must exist.' % filename)
3910-
3911- with open(CAPTCHA_PATH) as f:
3912- expected = f.read()
3913- with open(filename) as f:
3914- actual = f.read()
3915- self.assertEqual(expected, actual, 'captcha image must be correct.')
3916-
3917- def test_register_user_checks_valid_email(self):
3918- """Email is validated."""
3919- self.register_kwargs['email'] = 'notavalidemail'
3920- self.assertRaises(InvalidEmailError,
3921- self.processor.register_user, **self.register_kwargs)
3922-
3923- def test_register_user_checks_valid_password(self):
3924- """Password is validated."""
3925- self.register_kwargs['password'] = ''
3926- self.assertRaises(InvalidPasswordError,
3927- self.processor.register_user, **self.register_kwargs)
3928-
3929- # 7 chars, one less than expected
3930- self.register_kwargs['password'] = 'tesT3it'
3931- self.assertRaises(InvalidPasswordError,
3932- self.processor.register_user, **self.register_kwargs)
3933-
3934- self.register_kwargs['password'] = 'test3it!' # no upper case
3935- self.assertRaises(InvalidPasswordError,
3936- self.processor.register_user, **self.register_kwargs)
3937-
3938- self.register_kwargs['password'] = 'testIt!!' # no number
3939- self.assertRaises(InvalidPasswordError,
3940- self.processor.register_user, **self.register_kwargs)
3941-
3942- # register
3943-
3944- def test_register_user_if_status_ok(self):
3945- """A user is succesfuy registered into the SSO server."""
3946- result = self.processor.register_user(**self.register_kwargs)
3947- self.assertEqual(EMAIL, result, 'registration was successful.')
3948-
3949- def test_register_user_if_status_error(self):
3950- """Proper error is raised if register fails."""
3951- self.register_kwargs['captcha_id'] = CAPTCHA_ID * 2 # incorrect
3952- failure = self.assertRaises(RegistrationError,
3953- self.processor.register_user,
3954- **self.register_kwargs)
3955- for k, val in failure.args[0].items():
3956- self.assertIn(k, STATUS_ERROR['errors'])
3957- self.assertEqual(val, "\n".join(STATUS_ERROR['errors'][k]))
3958-
3959- def test_register_user_if_status_error_with_string_message(self):
3960- """Proper error is raised if register fails."""
3961- self.register_kwargs['email'] = EMAIL_ALREADY_REGISTERED
3962- failure = self.assertRaises(RegistrationError,
3963- self.processor.register_user,
3964- **self.register_kwargs)
3965- for k, val in failure.args[0].items():
3966- self.assertIn(k, {'email': 'Email already registered'})
3967- self.assertEqual(val, 'Email already registered')
3968-
3969- def test_register_user_if_status_unknown(self):
3970- """Proper error is raised if register returns an unknown status."""
3971- self.register_kwargs['captcha_id'] = None
3972- self.register_kwargs['captcha_solution'] = None
3973- failure = self.assertRaises(RegistrationError,
3974- self.processor.register_user,
3975- **self.register_kwargs)
3976- self.assertIn('Received unknown status: %s' % STATUS_UNKNOWN, failure)
3977-
3978- # login
3979-
3980- def test_login_if_http_error(self):
3981- """Proper error is raised if authentication fails."""
3982- self.login_kwargs['token_name'] = APP_NAME * 2 # invalid token name
3983- self.assertRaises(AuthenticationError,
3984- self.processor.login, **self.login_kwargs)
3985-
3986- def test_login_if_no_error(self):
3987- """A user can be succesfully logged in into the SSO service."""
3988- result = self.processor.login(**self.login_kwargs)
3989- self.assertEqual(TOKEN, result, 'authentication was successful.')
3990-
3991- # validate_email
3992-
3993- def test_validate_email_if_status_ok(self):
3994- """A email is succesfuy validated in the SSO server."""
3995- self.login_kwargs['email_token'] = EMAIL_TOKEN # valid email token
3996- result = self.processor.validate_email(**self.login_kwargs)
3997- self.assertEqual(TOKEN, result, 'email validation was successful.')
3998-
3999- def test_validate_email_if_status_error(self):
4000- """Proper error is raised if email validation fails."""
4001- self.login_kwargs['email_token'] = EMAIL_TOKEN * 2 # invalid token
4002- failure = self.assertRaises(EmailTokenError,
4003- self.processor.validate_email,
4004- **self.login_kwargs)
4005- for k, val in failure.args[0].items():
4006- self.assertIn(k, STATUS_EMAIL_ERROR['errors'])
4007- self.assertEqual(val, "\n".join(STATUS_EMAIL_ERROR['errors'][k]))
4008-
4009- def test_validate_email_if_status_error_with_string_message(self):
4010- """Proper error is raised if register fails."""
4011- self.login_kwargs['email_token'] = EMAIL_ALREADY_REGISTERED
4012- failure = self.assertRaises(EmailTokenError,
4013- self.processor.validate_email,
4014- **self.login_kwargs)
4015- for k, val in failure.args[0].items():
4016- self.assertIn(k, {'email': 'Email already registered'})
4017- self.assertEqual(val, 'Email already registered')
4018-
4019- def test_validate_email_if_status_unknown(self):
4020- """Proper error is raised if email validation returns unknown."""
4021- self.login_kwargs['email_token'] = None
4022- failure = self.assertRaises(EmailTokenError,
4023- self.processor.validate_email,
4024- **self.login_kwargs)
4025- self.assertIn('Received invalid reply: %s' % STATUS_UNKNOWN, failure)
4026-
4027- # reset_password
4028-
4029- def test_request_password_reset_token_if_status_ok(self):
4030- """A reset password token is succesfuly sent."""
4031- result = self.processor.request_password_reset_token(email=EMAIL)
4032- self.assertEqual(EMAIL, result,
4033- 'password reset token must be successful.')
4034-
4035- def test_request_password_reset_token_if_http_error(self):
4036- """Proper error is raised if password token request fails."""
4037- exc = self.assertRaises(ResetPasswordTokenError,
4038- self.processor.request_password_reset_token,
4039- email=EMAIL * 2)
4040- self.assertIn(CANT_RESET_PASSWORD_CONTENT, exc)
4041-
4042- def test_request_password_reset_token_if_status_unknown(self):
4043- """Proper error is raised if password token request returns unknown."""
4044- exc = self.assertRaises(ResetPasswordTokenError,
4045- self.processor.request_password_reset_token,
4046- email=None)
4047- self.assertIn('Received invalid reply: %s' % STATUS_UNKNOWN, exc)
4048-
4049- def test_set_new_password_if_status_ok(self):
4050- """A new password is succesfuy set."""
4051- result = self.processor.set_new_password(email=EMAIL,
4052- token=RESET_PASSWORD_TOKEN,
4053- new_password=PASSWORD)
4054- self.assertEqual(EMAIL, result,
4055- 'new password must be set successfully.')
4056-
4057- def test_set_new_password_if_http_error(self):
4058- """Proper error is raised if setting a new password fails."""
4059- exc = self.assertRaises(NewPasswordError,
4060- self.processor.set_new_password,
4061- email=EMAIL * 2,
4062- token=RESET_PASSWORD_TOKEN * 2,
4063- new_password=PASSWORD)
4064- self.assertIn(RESET_TOKEN_INVALID_CONTENT, exc)
4065-
4066- def test_set_new_password_if_status_unknown(self):
4067- """Proper error is raised if setting a new password returns unknown."""
4068- exc = self.assertRaises(NewPasswordError,
4069- self.processor.set_new_password,
4070- email=None, token=None, new_password=None)
4071- self.assertIn('Received invalid reply: %s' % STATUS_UNKNOWN, exc)
4072-
4073-
4074 class BlockingSampleException(Exception):
4075 """The exception that will be thrown by the fake blocking."""
4076
4077
4078+def fake_ok_blocking(f, app, cb, eb):
4079+ """A fake blocking function that succeeds."""
4080+ cb(app, f())
4081+
4082+
4083+def fake_err_blocking(f, app, cb, eb):
4084+ """A fake blocking function that fails."""
4085+ try:
4086+ f()
4087+ except Exception, e: # pylint: disable=W0703
4088+ eb(app, except_to_errdict(e))
4089+ else:
4090+ eb(app, except_to_errdict(BlockingSampleException()))
4091+
4092+
4093 class SsoDbusTestCase(TestCase):
4094 """Test the SSOLogin DBus interface."""
4095
4096 def setUp(self):
4097 """Create the mocking bus."""
4098- self.real_blocking = ubuntu_sso.main.blocking
4099 self.mocker = Mocker()
4100 self.mockbusname = self.mocker.mock()
4101 mockbus = self.mocker.mock()
4102@@ -396,19 +94,6 @@
4103 self.mocker.verify()
4104 self.mocker.restore()
4105
4106- def fake_ok_blocking(self, f, app, cb, eb):
4107- """A fake blocking function that succeeds."""
4108- cb(app, f())
4109-
4110- def fake_err_blocking(self, f, app, cb, eb):
4111- """A fake blocking function that fails."""
4112- try:
4113- f()
4114- except Exception, e: # pylint: disable=W0703
4115- eb(app, except_to_errdict(e))
4116- else:
4117- eb(app, except_to_errdict(BlockingSampleException()))
4118-
4119 def test_creation(self):
4120 """Test that the object creation is successful."""
4121 self.mocker.replay()
4122@@ -429,7 +114,7 @@
4123 expected_result = "expected result"
4124 self.create_mock_processor().generate_captcha(filename)
4125 self.mocker.result(expected_result)
4126- self.patch(ubuntu_sso.main, "blocking", self.fake_ok_blocking)
4127+ self.patch(ubuntu_sso.main, "blocking", fake_ok_blocking)
4128 self.mocker.replay()
4129
4130 def verify(app_name, result):
4131@@ -452,7 +137,7 @@
4132 expected_result = "expected result"
4133 self.create_mock_processor().generate_captcha(filename)
4134 self.mocker.result(expected_result)
4135- self.patch(ubuntu_sso.main, "blocking", self.fake_err_blocking)
4136+ self.patch(ubuntu_sso.main, "blocking", fake_err_blocking)
4137 self.mocker.replay()
4138
4139 def verify(app_name, errdict):
4140@@ -475,7 +160,7 @@
4141 self.create_mock_processor().register_user(EMAIL, PASSWORD, CAPTCHA_ID,
4142 CAPTCHA_SOLUTION)
4143 self.mocker.result(expected_result)
4144- self.patch(ubuntu_sso.main, "blocking", self.fake_ok_blocking)
4145+ self.patch(ubuntu_sso.main, "blocking", fake_ok_blocking)
4146 self.mocker.replay()
4147
4148 def verify(app_name, result):
4149@@ -499,7 +184,7 @@
4150 self.create_mock_processor().register_user(EMAIL, PASSWORD, CAPTCHA_ID,
4151 CAPTCHA_SOLUTION)
4152 self.mocker.result(expected_result)
4153- self.patch(ubuntu_sso.main, "blocking", self.fake_err_blocking)
4154+ self.patch(ubuntu_sso.main, "blocking", fake_err_blocking)
4155 self.mocker.replay()
4156
4157 def verify(app_name, errdict):
4158@@ -521,7 +206,7 @@
4159 d = Deferred()
4160 self.create_mock_processor().login(EMAIL, PASSWORD, TOKEN_NAME)
4161 self.mocker.result(TOKEN)
4162- self.patch(ubuntu_sso.main, "blocking", self.fake_ok_blocking)
4163+ self.patch(ubuntu_sso.main, "blocking", fake_ok_blocking)
4164 self.mocker.replay()
4165
4166 def verify(app_name, result):
4167@@ -542,7 +227,7 @@
4168 """Test that the login method fails as expected."""
4169 d = Deferred()
4170 self.mockprocessorclass = self.mocker.mock()
4171- self.patch(ubuntu_sso.main, "blocking", self.fake_err_blocking)
4172+ self.patch(ubuntu_sso.main, "blocking", fake_err_blocking)
4173
4174 def fake_gtn(*args):
4175 """A fake get_token_name that fails."""
4176@@ -571,7 +256,7 @@
4177 self.create_mock_processor().validate_email(EMAIL, PASSWORD,
4178 EMAIL_TOKEN, TOKEN_NAME)
4179 self.mocker.result(TOKEN)
4180- self.patch(ubuntu_sso.main, "blocking", self.fake_ok_blocking)
4181+ self.patch(ubuntu_sso.main, "blocking", fake_ok_blocking)
4182 self.mocker.replay()
4183
4184 def verify(app_name, result):
4185@@ -592,7 +277,7 @@
4186 """Test that the validate_email method fails as expected."""
4187 d = Deferred()
4188 self.mockprocessorclass = self.mocker.mock()
4189- self.patch(ubuntu_sso.main, "blocking", self.fake_err_blocking)
4190+ self.patch(ubuntu_sso.main, "blocking", fake_err_blocking)
4191
4192 def fake_gtn(*args):
4193 """A fake get_token_name that fails."""
4194@@ -620,7 +305,7 @@
4195 d = Deferred()
4196 processor = self.create_mock_processor()
4197 processor.request_password_reset_token(EMAIL)
4198- self.patch(ubuntu_sso.main, "blocking", self.fake_ok_blocking)
4199+ self.patch(ubuntu_sso.main, "blocking", fake_ok_blocking)
4200 self.mocker.result(EMAIL)
4201 self.mocker.replay()
4202
4203@@ -643,7 +328,7 @@
4204
4205 self.create_mock_processor().request_password_reset_token(EMAIL)
4206 self.mocker.result(EMAIL)
4207- self.patch(ubuntu_sso.main, "blocking", self.fake_err_blocking)
4208+ self.patch(ubuntu_sso.main, "blocking", fake_err_blocking)
4209 self.mocker.replay()
4210
4211 def verify(app_name, errdict):
4212@@ -665,7 +350,7 @@
4213 self.create_mock_processor().set_new_password(EMAIL, EMAIL_TOKEN,
4214 PASSWORD)
4215 self.mocker.result(EMAIL)
4216- self.patch(ubuntu_sso.main, "blocking", self.fake_ok_blocking)
4217+ self.patch(ubuntu_sso.main, "blocking", fake_ok_blocking)
4218 self.mocker.replay()
4219
4220 def verify(app_name, result):
4221@@ -689,7 +374,7 @@
4222 self.create_mock_processor().set_new_password(EMAIL, EMAIL_TOKEN,
4223 PASSWORD)
4224 self.mocker.result(expected_result)
4225- self.patch(ubuntu_sso.main, "blocking", self.fake_err_blocking)
4226+ self.patch(ubuntu_sso.main, "blocking", fake_err_blocking)
4227 self.mocker.replay()
4228
4229 def verify(app_name, errdict):
4230@@ -830,7 +515,7 @@
4231 self.mocker.result(TOKEN)
4232 self.mocker.replay()
4233
4234- token = keyring_get_credentials(APP_NAME)
4235+ token = credentials.keyring_get_credentials(APP_NAME)
4236 self.assertEqual(token, TOKEN)
4237
4238 def test_keyring_get_cred_not_found(self):
4239@@ -843,7 +528,7 @@
4240 self.mocker.result(None)
4241 self.mocker.replay()
4242
4243- token = keyring_get_credentials(APP_NAME)
4244+ token = credentials.keyring_get_credentials(APP_NAME)
4245 self.assertEqual(token, None)
4246
4247
4248@@ -851,521 +536,112 @@
4249 """A mock exception thrown just when testing."""
4250
4251
4252-class CredentialsTestCase(TestCase, MockerTestCase):
4253- """Tests for the credentials related DBus methods."""
4254-
4255- # Invalid name (should match ([a-z_][a-z0-9_]*|[A-Z_][A-Z0-9_]*)$)
4256- # pylint: disable=C0103
4257+class ApplicationCredentialsTestCase(TestCase, MockerTestCase):
4258+ """Tests for the ApplicationCredentials related DBus methods."""
4259
4260 timeout = 5
4261
4262+ def setUp(self):
4263+ MockerTestCase.setUp(self)
4264+
4265+ self.client = SSOCredentials(self.mocker.mock())
4266+
4267+ mock_class = self.mocker.replace("ubuntu_sso.credentials.Credentials")
4268+ mock_class(app_name=APP_NAME)
4269+ self.creds_obj = self.mocker.mock()
4270+ self.mocker.result(self.creds_obj)
4271+
4272 def test_find_credentials(self):
4273 """find_credentials immediately returns the token when found."""
4274 expected_token = "expected token"
4275- kgt = self.mocker.replace("ubuntu_sso.main.keyring_get_credentials")
4276- kgt(ARGS)
4277+ self.creds_obj.find_credentials()
4278 self.mocker.result(expected_token)
4279 self.mocker.replay()
4280
4281- client = SSOCredentials(self.mocker.mock())
4282- token = client.find_credentials(APP_NAME)
4283+ token = self.client.find_credentials(APP_NAME)
4284 self.assertEqual(token, expected_token)
4285
4286 def test_credentials_not_found(self):
4287- """find_credentials immediately returns '' when no token found."""
4288+ """find_credentials immediately returns {} when no token found."""
4289 expected_creds = {}
4290- kgt = self.mocker.replace("ubuntu_sso.main.keyring_get_credentials")
4291- kgt(ARGS)
4292- self.mocker.result(None)
4293+ self.creds_obj.find_credentials()
4294+ self.mocker.result(expected_creds)
4295 self.mocker.replay()
4296
4297- client = SSOCredentials(self.mocker.mock())
4298- token = client.find_credentials(APP_NAME)
4299+ token = self.client.find_credentials(APP_NAME)
4300 self.assertEqual(token, expected_creds)
4301
4302+
4303+class ApplicationCredentialsGUITestCase(TestCase, MockerTestCase):
4304+ """Tests for the ApplicationCredentials register/login DBus method."""
4305+
4306+ app_name = APP_NAME
4307+ ping_url = None
4308+
4309+ def setUp(self):
4310+ MockerTestCase.setUp(self)
4311+ self.client = SSOCredentials(self.mocker.mock())
4312+ self.args = {PING_URL_KEY: self.ping_url,
4313+ TC_URL_KEY: TC_URL, HELP_TEXT_KEY: HELP_TEXT,
4314+ WINDOW_ID_KEY: WINDOW_ID,
4315+ SUCCESS_CB_KEY: self.client.CredentialsFound,
4316+ ERROR_CB_KEY: self.client._process_error,
4317+ DENIAL_CB_KEY: self.client.AuthorizationDenied}
4318+
4319 def test_login_or_register(self):
4320- """login_or_register_... throws the signal when token is found."""
4321- expected_creds = TOKEN
4322- d = Deferred()
4323-
4324- def verify(app_name, credentials):
4325- """The actual test."""
4326- self.assertEqual(credentials, expected_creds)
4327- d.callback("ok")
4328-
4329- kgt = self.mocker.replace("ubuntu_sso.main.keyring_get_credentials")
4330- kgt(ARGS)
4331- self.mocker.result(expected_creds)
4332- self.mocker.replay()
4333- client = SSOCredentials(self.mocker.mock())
4334- self.patch(client, "_show_login_or_register_ui", self.fail)
4335- self.patch(client, "CredentialsFound", verify)
4336- self.patch(client, "CredentialsError", self.fail)
4337- client.login_or_register_to_get_credentials(*LOGIN_OR_REGISTER_ARGS)
4338- return d
4339-
4340- def test_login_or_register_not_found(self):
4341- """Check that login_or_register_... opens the ui when no cred found."""
4342- d = Deferred()
4343-
4344- def verify(result, *a):
4345- """The actual test."""
4346- self.assertEqual(result, APP_NAME)
4347- d.callback("ok")
4348-
4349- kgt = self.mocker.replace("ubuntu_sso.main.keyring_get_credentials")
4350- kgt(ARGS)
4351- self.mocker.result(None)
4352- self.mocker.replay()
4353-
4354- client = SSOCredentials(self.mocker.mock())
4355- self.patch(client, "_show_login_or_register_ui", verify)
4356- self.patch(client, "CredentialsFound", self.fail)
4357- self.patch(client, "CredentialsError", self.fail)
4358- client.login_or_register_to_get_credentials(*LOGIN_OR_REGISTER_ARGS)
4359- return d
4360-
4361- def test_login_or_register_problem(self):
4362- """login_or_register_... returns the right signal on error."""
4363- expected_error = "Sample Error - not for resale"
4364- d = Deferred()
4365-
4366- def verify(app_name, error_message, detailed_error):
4367- """The actual test."""
4368- self.assertEqual(app_name, APP_NAME)
4369- d.callback("ok")
4370-
4371- kgt = self.mocker.replace("ubuntu_sso.main.keyring_get_credentials")
4372- kgt(ARGS)
4373- self.mocker.throw(RegisterSampleException(expected_error))
4374- self.mocker.replay()
4375-
4376- client = SSOCredentials(self.mocker.mock())
4377- self.patch(client, "_show_login_or_register_ui", self.fail)
4378- self.patch(client, "CredentialsFound", self.fail)
4379- self.patch(client, "CredentialsError", verify)
4380- client.login_or_register_to_get_credentials(*LOGIN_OR_REGISTER_ARGS)
4381- return d
4382+ """login_or_register is correct."""
4383+ mock_class = self.mocker.replace("ubuntu_sso.credentials.Credentials")
4384+ mock_class(app_name=self.app_name, **self.args)
4385+ creds_obj = self.mocker.mock()
4386+ self.mocker.result(creds_obj)
4387+
4388+ creds_obj.register()
4389+ self.mocker.replay()
4390+
4391+ args = (self.app_name, TC_URL, HELP_TEXT, WINDOW_ID)
4392+ self.client.login_or_register_to_get_credentials(*args)
4393
4394 def test_login_only(self):
4395- """login_only_... throws the signal when token is found."""
4396- expected_creds = TOKEN
4397- d = Deferred()
4398-
4399- def verify(app_name, credentials):
4400- """The actual test."""
4401- self.assertEqual(credentials, expected_creds)
4402- d.callback("ok")
4403-
4404- kgt = self.mocker.replace("ubuntu_sso.main.keyring_get_credentials")
4405- kgt(ARGS)
4406- self.mocker.result(expected_creds)
4407- self.mocker.replay()
4408- client = SSOCredentials(self.mocker.mock())
4409- self.patch(client, "_show_login_only_ui", self.fail)
4410- self.patch(client, "CredentialsFound", verify)
4411- self.patch(client, "CredentialsError", self.fail)
4412- client.login_to_get_credentials(*LOGIN_ONLY_ARGS)
4413- return d
4414-
4415- def test_login_only_not_found(self):
4416- """Check that login_only_... opens the ui when no cred found."""
4417- d = Deferred()
4418-
4419- def verify(result, *a):
4420- """The actual test."""
4421- self.assertEqual(result, APP_NAME)
4422- d.callback("ok")
4423-
4424- kgt = self.mocker.replace("ubuntu_sso.main.keyring_get_credentials")
4425- kgt(ARGS)
4426- self.mocker.result(None)
4427- self.mocker.replay()
4428-
4429- client = SSOCredentials(self.mocker.mock())
4430- self.patch(client, "_show_login_only_ui", verify)
4431- self.patch(client, "CredentialsFound", self.fail)
4432- self.patch(client, "CredentialsError", self.fail)
4433- client.login_to_get_credentials(*LOGIN_ONLY_ARGS)
4434- return d
4435-
4436- def test_login_only_problem(self):
4437- """login_only_... returns the right signal on error."""
4438- expected_error = "Sample Error - not for resale"
4439- d = Deferred()
4440-
4441- def verify(app_name, error_message, detailed_error):
4442- """The actual test."""
4443- self.assertEqual(app_name, APP_NAME)
4444- d.callback("ok")
4445-
4446- kgt = self.mocker.replace("ubuntu_sso.main.keyring_get_credentials")
4447- kgt(ARGS)
4448- self.mocker.throw(RegisterSampleException(expected_error))
4449- self.mocker.replay()
4450-
4451- client = SSOCredentials(self.mocker.mock())
4452- self.patch(client, "_show_login_only_ui", self.fail)
4453- self.patch(client, "CredentialsFound", self.fail)
4454- self.patch(client, "CredentialsError", verify)
4455- client.login_to_get_credentials(*LOGIN_ONLY_ARGS)
4456- return d
4457+ """login_or_register is correct."""
4458+ self.args[TC_URL_KEY] = None
4459+ mock_class = self.mocker.replace("ubuntu_sso.credentials.Credentials")
4460+ mock_class(app_name=self.app_name, **self.args)
4461+ creds_obj = self.mocker.mock()
4462+ self.mocker.result(creds_obj)
4463+
4464+ creds_obj.login()
4465+ self.mocker.replay()
4466+
4467+ args = (self.app_name, HELP_TEXT, WINDOW_ID)
4468+ self.client.login_to_get_credentials(*args)
4469+
4470+
4471+class ApplicationCredentialsU1TestCase(ApplicationCredentialsGUITestCase):
4472+ """Tests for the ApplicationCredentials register/login DBus method.
4473+
4474+ Specifically for APP_NAME == U1_APP_NAME.
4475+
4476+ """
4477+
4478+ app_name = U1_APP_NAME
4479+ ping_url = U1_PING_URL
4480+
4481+
4482+class ApplicationCredentialsClearTokenTestCase(TestCase, MockerTestCase):
4483+ """Tests for the ApplicationCredentials related DBus methods."""
4484
4485 def test_clear_token(self):
4486 """Check that clear_token tries removing the correct token."""
4487- mockKeyringClass = self.mocker.replace("ubuntu_sso.keyring.Keyring")
4488- mockKeyringClass(APP_NAME)
4489- mockKeyring = self.mocker.mock()
4490- self.mocker.result(mockKeyring)
4491- mockKeyring.delete_ubuntusso_attr()
4492- self.mocker.replay()
4493-
4494- client = SSOCredentials(self.mocker.mock())
4495- client.clear_token(APP_NAME)
4496-
4497- def test_clear_token_failed(self):
4498- """Check that clear_token fails correctly."""
4499- mockKeyringClass = self.mocker.replace("ubuntu_sso.keyring.Keyring")
4500- mockKeyringClass(APP_NAME)
4501- self.mocker.throw(self.mocker.mock())
4502- fake_logger = self.mocker.replace("ubuntu_sso.main.logger")
4503- fake_logger.exception(ANY, APP_NAME)
4504- self.mocker.replay()
4505-
4506- client = SSOCredentials(self.mocker.mock())
4507- client.clear_token(APP_NAME)
4508-
4509- def test_login_error_cb(self):
4510- """The login error callback should throw the signal."""
4511- d = Deferred()
4512-
4513- def verify(app_name, error_message, detailed_error):
4514- """The actual test."""
4515- self.assertEqual(app_name, APP_NAME)
4516- d.callback("ok")
4517-
4518- self.mocker.replay()
4519- client = SSOCredentials(self.mocker.mock())
4520- self.patch(client, "CredentialsError", verify)
4521-
4522- client._login_error_cb(None, APP_NAME, 'some error')
4523- return d
4524-
4525- def test_show_login_or_register_ui_error(self):
4526- """An error happens when trying to register."""
4527- d = Deferred()
4528- mockgui = self.mocker.replace("ubuntu_sso.gui.UbuntuSSOClientGUI")
4529- mockgui(ARGS)
4530- self.mocker.throw(Exception())
4531- self.mocker.replay()
4532-
4533- def verify(app_name, msg, full_error):
4534- """The actual test."""
4535- self.assertEqual(app_name, APP_NAME)
4536- d.callback("ok")
4537-
4538- client = SSOCredentials(self.mocker.mock())
4539- self.patch(client, "CredentialsError", verify)
4540-
4541- client._show_login_or_register_ui(APP_NAME, "http:tc_url", "help", 0)
4542- return d
4543-
4544- def test_login_success_cb_works(self):
4545- """Check that the right signal is sent."""
4546- expected_creds = TOKEN
4547- d = Deferred()
4548- kgc = self.mocker.replace("ubuntu_sso.main.keyring_get_credentials")
4549- kgc(ARGS)
4550- self.mocker.result(expected_creds)
4551- self.mocker.replay()
4552-
4553- def verify(app_name, credentials):
4554- """The actual test."""
4555- self.assertEqual(credentials, expected_creds)
4556- d.callback("ok")
4557-
4558- client = SSOCredentials(self.mocker.mock())
4559- self.patch(client, "CredentialsFound", verify)
4560-
4561- client._login_success_cb(None, APP_NAME, EMAIL)
4562- return d
4563-
4564- def test_login_success_cb_error(self):
4565- """An error happens when accessing the keyring."""
4566- d = Deferred()
4567- kgc = self.mocker.replace("ubuntu_sso.main.keyring_get_credentials")
4568- kgc(ARGS)
4569- self.mocker.throw(Exception())
4570- self.mocker.replay()
4571-
4572- def verify(app_name, msg, full_error):
4573- """The actual test."""
4574- self.assertEqual(app_name, APP_NAME)
4575- d.callback("ok")
4576-
4577- client = SSOCredentials(self.mocker.mock())
4578- self.patch(client, "CredentialsError", verify)
4579-
4580- client._login_success_cb(None, APP_NAME, EMAIL)
4581- return d
4582-
4583- def test_auth_denied_cb(self):
4584- """When the user decides not to allow the registration or login."""
4585- d = Deferred()
4586- self.mocker.replay()
4587-
4588- def verify(app_name):
4589- """The actual test."""
4590- self.assertEqual(app_name, APP_NAME)
4591- d.callback("ok")
4592-
4593- client = SSOCredentials(self.mocker.mock())
4594- self.patch(client, "AuthorizationDenied", verify)
4595-
4596- client._login_auth_denied_cb(None, APP_NAME)
4597- return d
4598-
4599-
4600-class CredentialsGUITestCase(TestCase):
4601- """login_or_register_to_get_credentials opens the proper GUI."""
4602-
4603- def setUp(self):
4604- """Init."""
4605- self.args = None
4606- self.kwargs = None
4607-
4608- idle_add = lambda f, *args, **kwargs: f(*args, **kwargs)
4609- self.patch(gobject, "idle_add", idle_add)
4610- self.patch(ubuntu_sso.main, 'keyring_get_credentials', lambda n: None)
4611-
4612- self.memento = MementoHandler()
4613- self.memento.setLevel(logging.DEBUG)
4614- logger.addHandler(self.memento)
4615-
4616- self.client = SSOCredentials(None)
4617- self.login_or_register = \
4618- self.client.login_or_register_to_get_credentials
4619- self.login_only = self.client.login_to_get_credentials
4620-
4621- def test_login_or_register_opens_proper_ui(self):
4622- """The proper GUI is created if the token is None."""
4623-
4624- class FakedUbuntuSSOClientGUI(object):
4625- """Fake a SSO GUI."""
4626-
4627- # Method should have "self" as first argument
4628- # pylint: disable=E0213
4629-
4630- def __init__(sself, *args, **kwargs):
4631- self.args = args
4632- self.kwargs = kwargs
4633- sself.connect = lambda *a: None
4634-
4635- self.patch(gui, "UbuntuSSOClientGUI", FakedUbuntuSSOClientGUI)
4636- self.login_or_register(*LOGIN_OR_REGISTER_ARGS)
4637-
4638- self.assertEqual(self.args, LOGIN_OR_REGISTER_GUI_ARGS)
4639- self.assertEqual(self.kwargs, {})
4640-
4641- def test_ui_exceptions_are_logged(self):
4642- """If GUI fails the exception is logged."""
4643-
4644- def fail(*args, **kwargs):
4645- """Raise an exception."""
4646- self.args = AssertionError((args, kwargs))
4647- # pylint: disable=E0702
4648- raise self.args
4649-
4650- self.patch(gui, "UbuntuSSOClientGUI", fail)
4651- self.login_or_register(*LOGIN_OR_REGISTER_ARGS)
4652-
4653- self.assertTrue(1, len(self.memento.records))
4654- self.assertTrue(self.memento.check(logging.ERROR,
4655- str(LOGIN_OR_REGISTER_GUI_ARGS)))
4656- exc_info = self.memento.records[0].exc_info
4657- self.assertIn(self.args, exc_info)
4658-
4659- def test_ui_signals_are_connected(self):
4660- """Signals from GUI are properly connected."""
4661-
4662- self.args = []
4663-
4664- class FakedUbuntuSSOClientGUI(object):
4665- """Fake a SSO GUI."""
4666-
4667- # Method should have "self" as first argument
4668- # pylint: disable=E0213
4669-
4670- def __init__(sself, *args, **kwargs):
4671- sself.connect = lambda *a: self.args.append(a)
4672-
4673- expected = [
4674- (gui.SIG_LOGIN_SUCCEEDED, self.client._login_success_cb),
4675- (gui.SIG_LOGIN_FAILED, self.client._login_error_cb),
4676- (gui.SIG_REGISTRATION_SUCCEEDED, self.client._login_success_cb),
4677- (gui.SIG_REGISTRATION_FAILED, self.client._login_error_cb),
4678- (gui.SIG_USER_CANCELATION, self.client._login_auth_denied_cb),
4679- ]
4680- self.patch(gui, "UbuntuSSOClientGUI", FakedUbuntuSSOClientGUI)
4681- self.login_or_register(*LOGIN_OR_REGISTER_ARGS)
4682- self.assertEqual(self.args, expected)
4683-
4684- def test_login_only_opens_proper_ui(self):
4685- """The proper GUI is created if the token is None."""
4686-
4687- class FakedUbuntuSSOClientGUI(object):
4688- """Fake a SSO GUI."""
4689-
4690- # Method should have "self" as first argument
4691- # pylint: disable=E0213
4692-
4693- def __init__(sself, *args, **kwargs):
4694- self.args = args
4695- self.kwargs = kwargs
4696- sself.connect = lambda *a: None
4697-
4698- self.patch(gui, "UbuntuSSOClientGUI", FakedUbuntuSSOClientGUI)
4699- self.login_only(*LOGIN_ONLY_ARGS)
4700-
4701- self.assertEqual(self.args, LOGIN_ONLY_GUI_ARGS)
4702- self.assertEqual(self.kwargs, {})
4703-
4704- def test_login_only_ui_exceptions_are_logged(self):
4705- """If GUI fails the exception is logged."""
4706-
4707- def fail(*args, **kwargs):
4708- """Raise an exception."""
4709- self.args = AssertionError((args, kwargs))
4710- # pylint: disable=E0702
4711- raise self.args
4712-
4713- self.patch(gui, "UbuntuSSOClientGUI", fail)
4714- self.login_only(*LOGIN_ONLY_ARGS)
4715-
4716- self.assertTrue(1, len(self.memento.records))
4717- self.assertTrue(self.memento.check(logging.ERROR,
4718- str(LOGIN_ONLY_GUI_ARGS)))
4719- exc_info = self.memento.records[0].exc_info
4720- self.assertIn(self.args, exc_info)
4721-
4722- def test_login_only_ui_signals_are_connected(self):
4723- """Signals from GUI are properly connected."""
4724-
4725- self.args = []
4726-
4727- class FakedUbuntuSSOClientGUI(object):
4728- """Fake a SSO GUI."""
4729-
4730- # Method should have "self" as first argument
4731- # pylint: disable=E0213
4732-
4733- def __init__(sself, *args, **kwargs):
4734- sself.connect = lambda *a: self.args.append(a)
4735-
4736- expected = [
4737- (gui.SIG_LOGIN_SUCCEEDED, self.client._login_success_cb),
4738- (gui.SIG_LOGIN_FAILED, self.client._login_error_cb),
4739- (gui.SIG_REGISTRATION_SUCCEEDED, self.client._login_success_cb),
4740- (gui.SIG_REGISTRATION_FAILED, self.client._login_error_cb),
4741- (gui.SIG_USER_CANCELATION, self.client._login_auth_denied_cb),
4742- ]
4743- self.patch(gui, "UbuntuSSOClientGUI", FakedUbuntuSSOClientGUI)
4744- self.login_only(*LOGIN_ONLY_ARGS)
4745- self.assertEqual(self.args, expected)
4746-
4747-
4748-class PingServerTestCase(TestCase):
4749- """On successful login/registration, the U1 server is pinged."""
4750-
4751- def setUp(self):
4752- """Init."""
4753- self.patch(ubuntu_sso.main, 'keyring_get_credentials', lambda a: TOKEN)
4754- self.calls = []
4755- self.args = None
4756- self.kwargs = None
4757-
4758- def fake_it(*args, **kwargs):
4759- """Fake a call."""
4760- self.args = args
4761- self.kwargs = kwargs
4762- return FakedResponse(code=200)
4763-
4764- self.patch(urllib2, 'urlopen', fake_it)
4765-
4766- self.client = SSOCredentials(None)
4767-
4768- self.memento = MementoHandler()
4769- self.memento.setLevel(logging.DEBUG)
4770- logger.addHandler(self.memento)
4771-
4772- def _patch(self, name):
4773- """Patch a method so when it's called its name is stored."""
4774- self.patch(self.client, name,
4775- lambda *a, **kw: self.calls.append((name, a, kw)))
4776-
4777- def test_on_registration_successful_u1_server_is_pinged(self):
4778- """When a registration is successful, the PING_URL is pinged."""
4779- self.client._login_success_cb(None, U1_APP_NAME, EMAIL)
4780-
4781- self.assertEqual(len(self.args), 1)
4782- self.assertEqual(self.args[0].get_full_url(), PING_URL + EMAIL)
4783- self.assertEqual(self.kwargs, {})
4784-
4785- def test_on_registration_successful_no_server_is_pinged(self):
4786- """When a registration is successful, the PING_URL is not pinged."""
4787- self.client._login_success_cb(None, APP_NAME, EMAIL)
4788-
4789- self.assertEqual(self.args, None)
4790- self.assertEqual(self.kwargs, None)
4791-
4792- def test_ping_is_signed_with_credentials(self):
4793- """Ping to PING_URL is signed with user credentials."""
4794- result = self.client._ping_url(U1_APP_NAME, EMAIL, TOKEN)
4795- headers = self.args[0].headers
4796- self.assertIn('Authorization', headers)
4797- oauth_stuff = headers['Authorization']
4798-
4799- expected = 'oauth_consumer_key="xQ7xDAz", ' \
4800- 'oauth_signature_method="HMAC-SHA1", oauth_version="1.0", ' \
4801- 'oauth_token="GkInOfSMGwTXAUoVQwLUoPxElEEUdhsLVNTPhxHJDUIeHCPNEo' \
4802- '", oauth_signature="'
4803- self.assertIn(expected, oauth_stuff)
4804- self.assertEqual(result, 200)
4805-
4806- def test_ping_is_done_before_sending_signal(self):
4807- """Ping the server before sending any credential signal."""
4808- self._patch('_ping_url')
4809- self._patch('CredentialsFound')
4810- self._patch('CredentialsError')
4811- self._patch('AuthorizationDenied')
4812-
4813- self.client._login_success_cb(None, APP_NAME, EMAIL)
4814-
4815- self.assertEqual(len(self.calls), 2)
4816- self.assertEqual(self.calls[0], ('_ping_url',
4817- (APP_NAME, EMAIL, TOKEN), {}))
4818- self.assertEqual(self.calls[1], ('CredentialsFound',
4819- (APP_NAME, TOKEN), {}))
4820-
4821- def test_send_error_if_ping_failed(self):
4822- """Ping the server before sending any credential signal."""
4823-
4824- def fail(*args, **kwargs):
4825- """Raise an exception."""
4826- self.args = AssertionError((args, kwargs))
4827- # pylint: disable=E0702
4828- raise self.args
4829-
4830- self.patch(self.client, '_ping_url', fail)
4831- self._patch('CredentialsFound')
4832- self._patch('CredentialsError')
4833- self._patch('AuthorizationDenied')
4834-
4835- self.client._login_success_cb(None, APP_NAME, EMAIL)
4836-
4837- self.assertEqual(len(self.calls), 1)
4838- self.assertEqual(self.calls[0][0], 'CredentialsError')
4839- self.assertEqual(self.calls[0][1][0], APP_NAME)
4840+ mock_class = self.mocker.replace("ubuntu_sso.credentials.Credentials")
4841+ mock_class(app_name=APP_NAME)
4842+ creds_obj = self.mocker.mock()
4843+ self.mocker.result(creds_obj)
4844+
4845+ creds_obj.clear_credentials()
4846+ self.mocker.replay()
4847+
4848+ client = SSOCredentials(self.mocker.mock())
4849+ client.clear_token(APP_NAME)
4850
4851
4852 class EnvironOverridesTestCase(TestCase):
4853@@ -1388,23 +664,246 @@
4854 def test_no_override_ping_url(self):
4855 """If the environ is unset, the default ping url is used."""
4856 creds = SSOCredentials(None)
4857- self.assertEqual(creds.ping_url, PING_URL)
4858-
4859- def test_override_service_url(self):
4860- """The service url can be set from the env var USSOC_SERVICE_URL."""
4861- fake_url = 'this is not really a URL'
4862- old_url = os.environ.get('USSOC_SERVICE_URL')
4863- os.environ['USSOC_SERVICE_URL'] = fake_url
4864- try:
4865- proc = SSOLoginProcessor(sso_service_class=FakedSSOServer)
4866- self.assertEqual(proc.service_url, fake_url)
4867- finally:
4868- if old_url:
4869- os.environ['USSOC_SERVICE_URL'] = old_url
4870- else:
4871- del os.environ['USSOC_SERVICE_URL']
4872-
4873- def test_no_override_service_url(self):
4874- """If the environ is unset, the default service url is used."""
4875- proc = SSOLoginProcessor(sso_service_class=FakedSSOServer)
4876- self.assertEqual(proc.service_url, SERVICE_URL)
4877+ self.assertEqual(creds.ping_url, U1_PING_URL)
4878+
4879+
4880+class CredentialsManagementTestCase(TestCase):
4881+ """Tests for the CredentialsManagement DBus interface."""
4882+
4883+ base_args = {HELP_TEXT_KEY: HELP_TEXT, PING_URL_KEY: PING_URL,
4884+ TC_URL_KEY: TC_URL, WINDOW_ID_KEY: WINDOW_ID}
4885+
4886+ def setUp(self):
4887+ self.mocker = Mocker()
4888+ self.client = CredentialsManagement()
4889+ self.args = {}
4890+ self.cred_args = {}
4891+
4892+ def tearDown(self):
4893+ """Verify the mocking stuff and shut it down."""
4894+ self.mocker.verify()
4895+ self.mocker.restore()
4896+
4897+ def assert_dbus_method_correct(self, method):
4898+ """Check that 'method' is a dbus method with proper signatures."""
4899+ self.assertTrue(method._dbus_is_method)
4900+ self.assertEqual(method._dbus_interface, DBUS_CREDENTIALS_IFACE)
4901+ self.assertEqual(method._dbus_in_signature, 'sa{ss}')
4902+ self.assertEqual(method._dbus_out_signature, '')
4903+
4904+ def create_mock_backend(self):
4905+ """Create a mock backend."""
4906+ mock_class = self.mocker.replace("ubuntu_sso.credentials.Credentials")
4907+ mock_class(APP_NAME, **self.cred_args)
4908+ creds_obj = self.mocker.mock()
4909+ self.mocker.result(creds_obj)
4910+
4911+ return creds_obj
4912+
4913+ def test_is_dbus_object(self):
4914+ """CredentialsManagement is a Dbus object."""
4915+ self.assertIsInstance(self.client, ubuntu_sso.main.dbus.service.Object)
4916+
4917+
4918+class CredentialsManagementFindClearTestCase(CredentialsManagementTestCase):
4919+ """Tests for the CredentialsManagement find/clear methods."""
4920+
4921+ timeout = 5
4922+
4923+ def test_find_credentials(self):
4924+ """The credentials are asked and returned in signals."""
4925+ self.create_mock_backend().find_credentials()
4926+ self.mocker.replay()
4927+
4928+ self.client.find_credentials(APP_NAME, self.args)
4929+ self.assert_dbus_method_correct(self.client.find_credentials)
4930+
4931+ def test_find_credentials_does_not_block_when_found(self):
4932+ """Calling find_credentials does not block but return thru signals.
4933+
4934+ If the creds are found, CredentialsFound is emitted.
4935+
4936+ """
4937+ d = Deferred()
4938+
4939+ def verify(app_name, creds):
4940+ """The actual test."""
4941+ try:
4942+ self.assertEqual(app_name, APP_NAME)
4943+ self.assertEqual(creds, TOKEN)
4944+ except Exception, e: # pylint: disable=W0703
4945+ d.errback(e)
4946+ else:
4947+ d.callback(creds)
4948+
4949+ self.patch(ubuntu_sso.main, 'blocking', fake_ok_blocking)
4950+ self.patch(self.client, 'CredentialsFound', verify)
4951+ self.patch(self.client, 'CredentialsNotFound', d.errback)
4952+
4953+ self.create_mock_backend().find_credentials()
4954+ self.mocker.result(TOKEN)
4955+ self.mocker.replay()
4956+
4957+ self.client.find_credentials(APP_NAME, self.args)
4958+ return d
4959+
4960+ def test_find_credentials_does_not_block_when_not_found(self):
4961+ """Calling find_credentials does not block but return thru signals.
4962+
4963+ If the creds are not found, CredentialsNotFound is emitted.
4964+
4965+ """
4966+ d = Deferred()
4967+
4968+ def verify(app_name):
4969+ """The actual test."""
4970+ try:
4971+ self.assertEqual(app_name, APP_NAME)
4972+ except Exception, e: # pylint: disable=W0703
4973+ d.errback(e)
4974+ else:
4975+ d.callback(app_name)
4976+
4977+ self.patch(ubuntu_sso.main, 'blocking', fake_ok_blocking)
4978+ self.patch(self.client, 'CredentialsFound', d.errback)
4979+ self.patch(self.client, 'CredentialsNotFound', verify)
4980+
4981+ self.create_mock_backend().find_credentials()
4982+ self.mocker.result({})
4983+ self.mocker.replay()
4984+
4985+ self.client.find_credentials(APP_NAME, self.args)
4986+ return d
4987+
4988+ def test_clear_credentials(self):
4989+ """The credentials are removed."""
4990+ self.create_mock_backend().clear_credentials()
4991+ self.mocker.replay()
4992+
4993+ self.client.clear_credentials(APP_NAME, self.args)
4994+ self.assert_dbus_method_correct(self.client.clear_credentials)
4995+
4996+ def test_clear_credentials_does_not_block(self):
4997+ """Calling clear_credentials does not block but return thru signals."""
4998+ d = Deferred()
4999+
5000+ def verify(app_name):
The diff has been truncated for viewing.

Subscribers

People subscribed via source and target branches