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

Proposed by Natalia Bidart on 2010-08-24
Status: Merged
Merged at revision: 7
Proposed branch: lp:~nataliabidart/ubuntu/maverick/ubuntu-sso-client/ubuntu-sso-client-0.99.2
Merge into: lp:ubuntu/maverick/ubuntu-sso-client
Diff against target: 4698 lines (+801/-2717)
17 files modified
LICENSE.mocker (+0/-259)
MANIFEST.in (+2/-2)
PKG-INFO (+18/-2)
bin/show_nm_state (+41/-0)
bin/show_nm_state.py (+0/-39)
bin/ubuntu-sso-login (+13/-23)
bin/ubuntu-sso-login-gui (+2/-2)
contrib/dbus_util.py (+2/-4)
data/ui.glade (+4/-3)
debian/changelog (+30/-0)
debian/copyright (+0/-5)
mocker.py (+0/-2073)
setup.py (+13/-4)
ubuntu_sso/gui.py (+77/-57)
ubuntu_sso/main.py (+161/-95)
ubuntu_sso/tests/test_gui.py (+134/-71)
ubuntu_sso/tests/test_main.py (+304/-78)
To merge this branch: bzr merge lp:~nataliabidart/ubuntu/maverick/ubuntu-sso-client/ubuntu-sso-client-0.99.2
Reviewer Review Type Date Requested Status
Sebastien Bacher Needs Fixing on 2010-08-24
Ubuntu branches 2010-08-24 Pending
Review via email: mp+33546@code.launchpad.net

Description of the Change

Release 0.99.2.

To post a comment you must log in.
Sebastien Bacher (seb128) wrote :

the build-depends on python-mocker doesn't seem required since you don't run the testsuit during the build

review: Needs Fixing
Natalia Bidart (nataliabidart) wrote :

> the build-depends on python-mocker doesn't seem required since you don't run
> the testsuit during the build

Fixed and pushed!

Thanks

8. By Natalia Bidart on 2010-08-24

Removed python-mocker as a build-dep, and remove copyright notice for mocker.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== removed file 'LICENSE.mocker'
2--- LICENSE.mocker 2010-06-16 15:11:04 +0000
3+++ LICENSE.mocker 1970-01-01 00:00:00 +0000
4@@ -1,259 +0,0 @@
5-A. HISTORY OF THE SOFTWARE
6-==========================
7-
8-Python was created in the early 1990s by Guido van Rossum at Stichting
9-Mathematisch Centrum (CWI, see http://www.cwi.nl) in the Netherlands
10-as a successor of a language called ABC. Guido remains Python's
11-principal author, although it includes many contributions from others.
12-
13-In 1995, Guido continued his work on Python at the Corporation for
14-National Research Initiatives (CNRI, see http://www.cnri.reston.va.us)
15-in Reston, Virginia where he released several versions of the
16-software.
17-
18-In May 2000, Guido and the Python core development team moved to
19-BeOpen.com to form the BeOpen PythonLabs team. In October of the same
20-year, the PythonLabs team moved to Digital Creations (now Zope
21-Corporation, see http://www.zope.com). In 2001, the Python Software
22-Foundation (PSF, see http://www.python.org/psf/) was formed, a
23-non-profit organization created specifically to own Python-related
24-Intellectual Property. Zope Corporation is a sponsoring member of
25-the PSF.
26-
27-All Python releases are Open Source (see http://www.opensource.org for
28-the Open Source Definition). Historically, most, but not all, Python
29-releases have also been GPL-compatible; the table below summarizes
30-the various releases.
31-
32- Release Derived Year Owner GPL-
33- from compatible? (1)
34-
35- 0.9.0 thru 1.2 1991-1995 CWI yes
36- 1.3 thru 1.5.2 1.2 1995-1999 CNRI yes
37- 1.6 1.5.2 2000 CNRI no
38- 2.0 1.6 2000 BeOpen.com no
39- 1.6.1 1.6 2001 CNRI yes (2)
40- 2.1 2.0+1.6.1 2001 PSF no
41- 2.0.1 2.0+1.6.1 2001 PSF yes
42- 2.1.1 2.1+2.0.1 2001 PSF yes
43- 2.2 2.1.1 2001 PSF yes
44- 2.1.2 2.1.1 2002 PSF yes
45- 2.1.3 2.1.2 2002 PSF yes
46- 2.2.1 2.2 2002 PSF yes
47- 2.2.2 2.2.1 2002 PSF yes
48- 2.2.3 2.2.2 2003 PSF yes
49- 2.3 2.2.2 2002-2003 PSF yes
50-
51-Footnotes:
52-
53-(1) GPL-compatible doesn't mean that we're distributing Python under
54- the GPL. All Python licenses, unlike the GPL, let you distribute
55- a modified version without making your changes open source. The
56- GPL-compatible licenses make it possible to combine Python with
57- other software that is released under the GPL; the others don't.
58-
59-(2) According to Richard Stallman, 1.6.1 is not GPL-compatible,
60- because its license has a choice of law clause. According to
61- CNRI, however, Stallman's lawyer has told CNRI's lawyer that 1.6.1
62- is "not incompatible" with the GPL.
63-
64-Thanks to the many outside volunteers who have worked under Guido's
65-direction to make these releases possible.
66-
67-
68-B. TERMS AND CONDITIONS FOR ACCESSING OR OTHERWISE USING PYTHON
69-===============================================================
70-
71-PSF LICENSE AGREEMENT FOR PYTHON 2.3
72-------------------------------------
73-
74-1. This LICENSE AGREEMENT is between the Python Software Foundation
75-("PSF"), and the Individual or Organization ("Licensee") accessing and
76-otherwise using Python 2.3 software in source or binary form and its
77-associated documentation.
78-
79-2. Subject to the terms and conditions of this License Agreement, PSF
80-hereby grants Licensee a nonexclusive, royalty-free, world-wide
81-license to reproduce, analyze, test, perform and/or display publicly,
82-prepare derivative works, distribute, and otherwise use Python 2.3
83-alone or in any derivative version, provided, however, that PSF's
84-License Agreement and PSF's notice of copyright, i.e., "Copyright (c)
85-2001, 2002, 2003 Python Software Foundation; All Rights Reserved" are
86-retained in Python 2.3 alone or in any derivative version prepared by
87-Licensee.
88-
89-3. In the event Licensee prepares a derivative work that is based on
90-or incorporates Python 2.3 or any part thereof, and wants to make
91-the derivative work available to others as provided herein, then
92-Licensee hereby agrees to include in any such work a brief summary of
93-the changes made to Python 2.3.
94-
95-4. PSF is making Python 2.3 available to Licensee on an "AS IS"
96-basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
97-IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND
98-DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
99-FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON 2.3 WILL NOT
100-INFRINGE ANY THIRD PARTY RIGHTS.
101-
102-5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON
103-2.3 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS
104-A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 2.3,
105-OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
106-
107-6. This License Agreement will automatically terminate upon a material
108-breach of its terms and conditions.
109-
110-7. Nothing in this License Agreement shall be deemed to create any
111-relationship of agency, partnership, or joint venture between PSF and
112-Licensee. This License Agreement does not grant permission to use PSF
113-trademarks or trade name in a trademark sense to endorse or promote
114-products or services of Licensee, or any third party.
115-
116-8. By copying, installing or otherwise using Python 2.3, Licensee
117-agrees to be bound by the terms and conditions of this License
118-Agreement.
119-
120-
121-BEOPEN.COM LICENSE AGREEMENT FOR PYTHON 2.0
122--------------------------------------------
123-
124-BEOPEN PYTHON OPEN SOURCE LICENSE AGREEMENT VERSION 1
125-
126-1. This LICENSE AGREEMENT is between BeOpen.com ("BeOpen"), having an
127-office at 160 Saratoga Avenue, Santa Clara, CA 95051, and the
128-Individual or Organization ("Licensee") accessing and otherwise using
129-this software in source or binary form and its associated
130-documentation ("the Software").
131-
132-2. Subject to the terms and conditions of this BeOpen Python License
133-Agreement, BeOpen hereby grants Licensee a non-exclusive,
134-royalty-free, world-wide license to reproduce, analyze, test, perform
135-and/or display publicly, prepare derivative works, distribute, and
136-otherwise use the Software alone or in any derivative version,
137-provided, however, that the BeOpen Python License is retained in the
138-Software, alone or in any derivative version prepared by Licensee.
139-
140-3. BeOpen is making the Software available to Licensee on an "AS IS"
141-basis. BEOPEN MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
142-IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, BEOPEN MAKES NO AND
143-DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
144-FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE WILL NOT
145-INFRINGE ANY THIRD PARTY RIGHTS.
146-
147-4. BEOPEN SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF THE
148-SOFTWARE FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS
149-AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THE SOFTWARE, OR ANY
150-DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
151-
152-5. This License Agreement will automatically terminate upon a material
153-breach of its terms and conditions.
154-
155-6. This License Agreement shall be governed by and interpreted in all
156-respects by the law of the State of California, excluding conflict of
157-law provisions. Nothing in this License Agreement shall be deemed to
158-create any relationship of agency, partnership, or joint venture
159-between BeOpen and Licensee. This License Agreement does not grant
160-permission to use BeOpen trademarks or trade names in a trademark
161-sense to endorse or promote products or services of Licensee, or any
162-third party. As an exception, the "BeOpen Python" logos available at
163-http://www.pythonlabs.com/logos.html may be used according to the
164-permissions granted on that web page.
165-
166-7. By copying, installing or otherwise using the software, Licensee
167-agrees to be bound by the terms and conditions of this License
168-Agreement.
169-
170-
171-CNRI LICENSE AGREEMENT FOR PYTHON 1.6.1
172----------------------------------------
173-
174-1. This LICENSE AGREEMENT is between the Corporation for National
175-Research Initiatives, having an office at 1895 Preston White Drive,
176-Reston, VA 20191 ("CNRI"), and the Individual or Organization
177-("Licensee") accessing and otherwise using Python 1.6.1 software in
178-source or binary form and its associated documentation.
179-
180-2. Subject to the terms and conditions of this License Agreement, CNRI
181-hereby grants Licensee a nonexclusive, royalty-free, world-wide
182-license to reproduce, analyze, test, perform and/or display publicly,
183-prepare derivative works, distribute, and otherwise use Python 1.6.1
184-alone or in any derivative version, provided, however, that CNRI's
185-License Agreement and CNRI's notice of copyright, i.e., "Copyright (c)
186-1995-2001 Corporation for National Research Initiatives; All Rights
187-Reserved" are retained in Python 1.6.1 alone or in any derivative
188-version prepared by Licensee. Alternately, in lieu of CNRI's License
189-Agreement, Licensee may substitute the following text (omitting the
190-quotes): "Python 1.6.1 is made available subject to the terms and
191-conditions in CNRI's License Agreement. This Agreement together with
192-Python 1.6.1 may be located on the Internet using the following
193-unique, persistent identifier (known as a handle): 1895.22/1013. This
194-Agreement may also be obtained from a proxy server on the Internet
195-using the following URL: http://hdl.handle.net/1895.22/1013".
196-
197-3. In the event Licensee prepares a derivative work that is based on
198-or incorporates Python 1.6.1 or any part thereof, and wants to make
199-the derivative work available to others as provided herein, then
200-Licensee hereby agrees to include in any such work a brief summary of
201-the changes made to Python 1.6.1.
202-
203-4. CNRI is making Python 1.6.1 available to Licensee on an "AS IS"
204-basis. CNRI MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
205-IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, CNRI MAKES NO AND
206-DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
207-FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON 1.6.1 WILL NOT
208-INFRINGE ANY THIRD PARTY RIGHTS.
209-
210-5. CNRI SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON
211-1.6.1 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS
212-A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 1.6.1,
213-OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
214-
215-6. This License Agreement will automatically terminate upon a material
216-breach of its terms and conditions.
217-
218-7. This License Agreement shall be governed by the federal
219-intellectual property law of the United States, including without
220-limitation the federal copyright law, and, to the extent such
221-U.S. federal law does not apply, by the law of the Commonwealth of
222-Virginia, excluding Virginia's conflict of law provisions.
223-Notwithstanding the foregoing, with regard to derivative works based
224-on Python 1.6.1 that incorporate non-separable material that was
225-previously distributed under the GNU General Public License (GPL), the
226-law of the Commonwealth of Virginia shall govern this License
227-Agreement only as to issues arising under or with respect to
228-Paragraphs 4, 5, and 7 of this License Agreement. Nothing in this
229-License Agreement shall be deemed to create any relationship of
230-agency, partnership, or joint venture between CNRI and Licensee. This
231-License Agreement does not grant permission to use CNRI trademarks or
232-trade name in a trademark sense to endorse or promote products or
233-services of Licensee, or any third party.
234-
235-8. By clicking on the "ACCEPT" button where indicated, or by copying,
236-installing or otherwise using Python 1.6.1, Licensee agrees to be
237-bound by the terms and conditions of this License Agreement.
238-
239- ACCEPT
240-
241-
242-CWI LICENSE AGREEMENT FOR PYTHON 0.9.0 THROUGH 1.2
243---------------------------------------------------
244-
245-Copyright (c) 1991 - 1995, Stichting Mathematisch Centrum Amsterdam,
246-The Netherlands. All rights reserved.
247-
248-Permission to use, copy, modify, and distribute this software and its
249-documentation for any purpose and without fee is hereby granted,
250-provided that the above copyright notice appear in all copies and that
251-both that copyright notice and this permission notice appear in
252-supporting documentation, and that the name of Stichting Mathematisch
253-Centrum or CWI not be used in advertising or publicity pertaining to
254-distribution of the software without specific, written prior
255-permission.
256-
257-STICHTING MATHEMATISCH CENTRUM DISCLAIMS ALL WARRANTIES WITH REGARD TO
258-THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
259-FITNESS, IN NO EVENT SHALL STICHTING MATHEMATISCH CENTRUM BE LIABLE
260-FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
261-WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
262-ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
263-OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
264
265=== modified file 'MANIFEST.in'
266--- MANIFEST.in 2010-08-11 17:03:11 +0000
267+++ MANIFEST.in 2010-08-24 20:27:39 +0000
268@@ -1,6 +1,6 @@
269 include MANIFEST.in
270-include COPYING LICENSE.mocker README
271-include run-tests mocker.py
272+include COPYING README
273+include run-tests
274 recursive-include bin *
275 recursive-include data *
276 recursive-include ubuntu_sso *.py
277
278=== modified file 'PKG-INFO'
279--- PKG-INFO 2010-08-19 16:02:09 +0000
280+++ PKG-INFO 2010-08-24 20:27:39 +0000
281@@ -1,6 +1,6 @@
282-Metadata-Version: 1.0
283+Metadata-Version: 1.1
284 Name: ubuntu-sso-client
285-Version: 0.99.1
286+Version: 0.99.2
287 Summary: Ubuntu Single Sign-On client
288 Home-page: https://launchpad.net/ubuntu-sso-client
289 Author: Natalia Bidart
290@@ -8,3 +8,19 @@
291 License: GPL v3
292 Description: Desktop service to allow applications to sign into Ubuntu services via SSO
293 Platform: UNKNOWN
294+Requires: contrib.testing.testcase
295+Requires: dbus
296+Requires: gnomekeyring
297+Requires: gobject
298+Requires: gtk
299+Requires: mocker
300+Requires: oauth
301+Requires: testresources
302+Requires: twisted.internet
303+Requires: twisted.python
304+Requires: twisted.trial.unittest
305+Requires: twisted.web
306+Requires: webkit
307+Requires: xdg
308+Requires: zope.interface
309+Provides: ubuntu_sso
310
311=== added file 'bin/show_nm_state'
312--- bin/show_nm_state 1970-01-01 00:00:00 +0000
313+++ bin/show_nm_state 2010-08-24 20:27:39 +0000
314@@ -0,0 +1,41 @@
315+#!/usr/bin/python
316+
317+# show_nm_state - Show the state of the NetworkManager daemon
318+#
319+# Author: Alejandro J. Cura <alecu@canonical.com>
320+#
321+# Copyright 2010 Canonical Ltd.
322+#
323+# This program is free software: you can redistribute it and/or modify it
324+# under the terms of the GNU General Public License version 3, as published
325+# by the Free Software Foundation.
326+#
327+# This program is distributed in the hope that it will be useful, but
328+# WITHOUT ANY WARRANTY; without even the implied warranties of
329+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
330+# PURPOSE. See the GNU General Public License for more details.
331+#
332+# You should have received a copy of the GNU General Public License along
333+# with this program. If not, see <http://www.gnu.org/licenses/>.
334+"""A test program that just shows the network state."""
335+
336+import gobject
337+
338+from dbus.mainloop.glib import DBusGMainLoop
339+
340+from ubuntu_sso.networkstate import NetworkManagerState, nm_state_names
341+
342+DBusGMainLoop(set_as_default=True)
343+loop = gobject.MainLoop()
344+
345+
346+def got_state(state):
347+ """Called when the network state is found."""
348+ try:
349+ print nm_state_names[state]
350+ finally:
351+ loop.quit()
352+
353+nms = NetworkManagerState(got_state)
354+nms.find_online_state()
355+loop.run()
356
357=== removed file 'bin/show_nm_state.py'
358--- bin/show_nm_state.py 2010-08-11 17:03:11 +0000
359+++ bin/show_nm_state.py 1970-01-01 00:00:00 +0000
360@@ -1,39 +0,0 @@
361-# show_nm_state - Show the state of the NetworkManager daemon
362-#
363-# Author: Alejandro J. Cura <alecu@canonical.com>
364-#
365-# Copyright 2010 Canonical Ltd.
366-#
367-# This program is free software: you can redistribute it and/or modify it
368-# under the terms of the GNU General Public License version 3, as published
369-# by the Free Software Foundation.
370-#
371-# This program is distributed in the hope that it will be useful, but
372-# WITHOUT ANY WARRANTY; without even the implied warranties of
373-# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
374-# PURPOSE. See the GNU General Public License for more details.
375-#
376-# You should have received a copy of the GNU General Public License along
377-# with this program. If not, see <http://www.gnu.org/licenses/>.
378-"""A test program that just shows the network state."""
379-
380-import gobject
381-
382-from dbus.mainloop.glib import DBusGMainLoop
383-
384-from ubuntu_sso.networkstate import NetworkManagerState, nm_state_names
385-
386-DBusGMainLoop(set_as_default=True)
387-loop = gobject.MainLoop()
388-
389-
390-def got_state(state):
391- """Called when the network state is found."""
392- try:
393- print nm_state_names[state]
394- finally:
395- loop.quit()
396-
397-nms = NetworkManagerState(got_state)
398-nms.find_online_state()
399-loop.run()
400
401=== modified file 'bin/ubuntu-sso-login'
402--- bin/ubuntu-sso-login 2010-08-11 17:03:11 +0000
403+++ bin/ubuntu-sso-login 2010-08-24 20:27:39 +0000
404@@ -18,13 +18,9 @@
405 # You should have received a copy of the GNU General Public License along
406 # with this program. If not, see <http://www.gnu.org/licenses/>.
407
408-import pygtk
409-pygtk.require('2.0')
410 import gtk
411 import pango
412 import sys
413-import gettext
414-#from ubuntu import clientdefs
415
416 import dbus.service
417
418@@ -39,9 +35,6 @@
419 gtk.gdk.threads_init()
420 DBusGMainLoop(set_as_default=True)
421
422-_ = gettext.gettext
423-ngettext = gettext.ngettext
424-
425 OAUTH_REALM = "https://ubuntuone.com"
426 OAUTH_CONSUMER = "ubuntuone"
427
428@@ -78,7 +71,7 @@
429 """Initializes the child threads and dbus monitor."""
430 gtk.rc_parse_string(RCSTYLE)
431
432- logger.info(_("Starting Ubuntu SSO login manager"))
433+ logger.info("Starting Ubuntu SSO login manager")
434
435 self.__bus = dbus.SessionBus()
436
437@@ -115,7 +108,7 @@
438
439 def auth_denied(self):
440 """Signal callback for when auth was denied by user."""
441- logger.info(_("Access to Ubuntu One was denied."))
442+ logger.info("Access to Ubuntu One was denied.")
443
444 from twisted.internet import reactor
445 reactor.stop()
446@@ -128,7 +121,7 @@
447
448 if message:
449 logger.error(message)
450- dialog = gtk.Dialog(title=_("Ubuntu One: Error"),
451+ dialog = gtk.Dialog(title="Ubuntu One: Error",
452 flags=gtk.DIALOG_NO_SEPARATOR,
453 buttons=(gtk.STOCK_CLOSE,
454 gtk.RESPONSE_CLOSE))
455@@ -152,7 +145,7 @@
456 vbox.show()
457 hbox.pack_start(vbox)
458
459- label = gtk.Label("<b>%s</b>" % _("Authorization Error"))
460+ label = gtk.Label("<b>Authorization Error</b>")
461 label.set_use_markup(True)
462 label.set_alignment(0.0, 0.5)
463 label.show()
464@@ -171,7 +164,7 @@
465
466 dialog.show()
467 else:
468- logger.error(_("Got an OAuth error with no message."))
469+ logger.error("Got an OAuth error with no message.")
470
471 def set_up_desktopcouch_pairing(self, consumer_key):
472 """Add a pairing record between desktopcouch and Ubuntu One"""
473@@ -181,8 +174,8 @@
474 from desktopcouch.records.server import CouchDatabase
475 except ImportError:
476 # desktopcouch is not installed
477- logger.debug(_("Not adding desktopcouch pairing since"
478- " desktopcouch is not installed"))
479+ logger.debug("Not adding desktopcouch pairing since desktopcouch "
480+ "is not installed")
481 return
482 # Check whether there is already a record of the Ubuntu One service
483 db = CouchDatabase("management", create=True)
484@@ -200,14 +193,14 @@
485 for row in results:
486 found = True
487 if row.value == 1:
488- logger.debug(_("Not adding desktopcouch pairing since"
489- " the user has explicitly unpaired with Ubuntu One"))
490+ logger.debug("Not adding desktopcouch pairing since the user "
491+ "has explicitly unpaired with Ubuntu One")
492 else:
493- logger.debug(_("Not adding desktopcouch pairing since"
494- " we are already paired"))
495+ logger.debug("Not adding desktopcouch pairing since we are "
496+ "already paired")
497 if not found:
498 put_static_paired_service(None, "ubuntuone")
499- logger.debug(_("Pairing desktopcouch with Ubuntu One"))
500+ logger.debug("Pairing desktopcouch with Ubuntu One")
501
502 def main(self):
503 """Starts the gtk main loop."""
504@@ -218,15 +211,12 @@
505
506
507 if __name__ == "__main__":
508- #gettext.bindtextdomain(clientdefs.GETTEXT_PACKAGE, clientdefs.LOCALEDIR)
509- #gettext.textdomain(clientdefs.GETTEXT_PACKAGE)
510-
511 # Register DBus service for making sure we run only one instance
512 bus = dbus.SessionBus()
513 name = bus.request_name(DBUS_BUS_NAME,
514 dbus.bus.NAME_FLAG_DO_NOT_QUEUE)
515 if name == dbus.bus.REQUEST_NAME_REPLY_EXISTS:
516- print _("Ubuntu One login manager already running, quitting")
517+ print "Ubuntu One login manager already running, quitting"
518 sys.exit(0)
519
520 from twisted.internet import gtk2reactor
521
522=== modified file 'bin/ubuntu-sso-login-gui'
523--- bin/ubuntu-sso-login-gui 2010-08-19 16:02:09 +0000
524+++ bin/ubuntu-sso-login-gui 2010-08-24 20:27:39 +0000
525@@ -28,8 +28,8 @@
526 APP_NAME = 'Ubuntu'
527 TC_URI = 'http://one.ubuntu.com/terms'
528 HELP_TEXT = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. ' \
529- 'Nam sed lorem nibh. Suspendisse gravida nulla non nunc suscipit ' \
530- 'pulvinar tempus ut augue.'
531+ 'Nam sed lorem nibh. Suspendisse gravida nulla non nunc suscipit' \
532+ ' pulvinar tempus ut augue.'
533
534 main_quit = lambda *args, **kwargs: gtk.main_quit()
535
536
537=== modified file 'contrib/dbus_util.py'
538--- contrib/dbus_util.py 2010-08-06 22:01:01 +0000
539+++ contrib/dbus_util.py 2010-08-24 20:27:39 +0000
540@@ -25,13 +25,11 @@
541
542
543 class DBusLaunchError(Exception):
544- """Error while launching dbus-daemon"""
545- pass
546+ """Error while launching dbus-daemon."""
547
548
549 class NotFoundError(Exception):
550- """Not found error"""
551- pass
552+ """Not found error."""
553
554
555 class DBusRunner(object):
556
557=== modified file 'data/ui.glade'
558--- data/ui.glade 2010-08-19 16:02:09 +0000
559+++ data/ui.glade 2010-08-24 20:27:39 +0000
560@@ -184,6 +184,7 @@
561 </child>
562 <child>
563 <object class="GtkCheckButton" id="yes_to_updates_checkbutton">
564+ <property name="label" translatable="yes">yes to updates</property>
565 <property name="visible">True</property>
566 <property name="can_focus">True</property>
567 <property name="receives_default">False</property>
568@@ -200,7 +201,7 @@
569 <property name="visible">True</property>
570 <child>
571 <object class="GtkCheckButton" id="yes_to_tc_checkbutton">
572- <property name="label" translatable="yes">Yes! I agree with</property>
573+ <property name="label" translatable="yes">yes to tc</property>
574 <property name="visible">True</property>
575 <property name="can_focus">True</property>
576 <property name="receives_default">False</property>
577@@ -214,7 +215,7 @@
578 </child>
579 <child>
580 <object class="GtkLinkButton" id="tc_button">
581- <property name="label" translatable="yes">button</property>
582+ <property name="label" translatable="yes">tc button</property>
583 <property name="visible">True</property>
584 <property name="can_focus">True</property>
585 <property name="receives_default">True</property>
586@@ -284,7 +285,7 @@
587 </child>
588 <child>
589 <object class="GtkLinkButton" id="login_button">
590- <property name="label" translatable="yes">button</property>
591+ <property name="label" translatable="yes">login button</property>
592 <property name="visible">True</property>
593 <property name="can_focus">True</property>
594 <property name="receives_default">True</property>
595
596=== modified file 'debian/changelog'
597--- debian/changelog 2010-08-19 19:02:55 +0000
598+++ debian/changelog 2010-08-24 20:27:39 +0000
599@@ -1,3 +1,33 @@
600+ubuntu-sso-client (0.99.2-0ubuntu1) UNRELEASED; urgency=low
601+
602+ * New upstream release:
603+
604+ [ Natalia Bidart ]
605+ * Ping an URL when the login was successful (LP: #621226).
606+ * Signals propagates the app_name to be later check by the GUI so only those
607+ belonging to it will be processed (LP: #621377).
608+
609+ [ Natalia Bidart ]
610+ * Added modifications to setup.py and a new po/ directory with the file that has
611+ strings that need translation (LP: #616515).
612+
613+ [ Rodrigo Moya ]
614+ * Add clear_token DBus API (LP: #622833).
615+
616+ [ Alejandro J. Cura ]
617+ * Make all the signals return the errors as dictionaries (LP: #616105).
618+ * Credential* signals should return the app_name as well (LP: #621377).
619+
620+ [ Alejandro J. Cura ]
621+ * Use Ubuntu One OAuth credentials if they are already stored in the keyring
622+ (LP: #617355).
623+
624+ [ Natalia Bidart ]
625+ * Final outcome (as signal) is emitted after user closes the main window (LP:
626+ #620008).
627+
628+ -- Natalia Bidart (nessita) <nataliabidart@gmail.com> Tue, 24 Aug 2010 12:50:57 -0300
629+
630 ubuntu-sso-client (0.99.1-0ubuntu1) maverick; urgency=low
631
632 * New upstream release:
633
634=== modified file 'debian/copyright'
635--- debian/copyright 2010-06-16 15:11:04 +0000
636+++ debian/copyright 2010-08-24 20:27:39 +0000
637@@ -3,11 +3,6 @@
638 Upstream-Maintainer: Natalia Bidart <natalia.bidart@canonical.com>
639 Upstream-Source: https://launchpad.net/ubuntu-sso-client
640
641-Files: mocker.py
642-Copyright: (C) 2007 Gustavo Niemeyer <gustavo@niemeyer.net>
643-License: PSF License
644- The full text of the PSF License is distributed in the file LICENSE.mocker.
645-
646 Files: *
647 Copyright: (C) 2010 Canonical Ltd.
648 License: GPL v3
649
650=== removed file 'mocker.py'
651--- mocker.py 2010-06-16 15:11:04 +0000
652+++ mocker.py 1970-01-01 00:00:00 +0000
653@@ -1,2073 +0,0 @@
654-"""
655-Copyright (c) 2007 Gustavo Niemeyer <gustavo@niemeyer.net>
656-
657-Graceful platform for test doubles in Python (mocks, stubs, fakes, and dummies).
658-"""
659-import __builtin__
660-import tempfile
661-import unittest
662-import inspect
663-import shutil
664-import types
665-import sys
666-import os
667-import gc
668-
669-
670-if sys.version_info < (2, 4):
671- from sets import Set as set # pragma: nocover
672-
673-
674-__all__ = ["Mocker", "expect", "IS", "CONTAINS", "IN", "MATCH",
675- "ANY", "ARGS", "KWARGS"]
676-
677-
678-__author__ = "Gustavo Niemeyer <gustavo@niemeyer.net>"
679-__license__ = "PSF License"
680-__version__ = "0.10.1"
681-
682-
683-ERROR_PREFIX = "[Mocker] "
684-
685-
686-# --------------------------------------------------------------------
687-# Exceptions
688-
689-class MatchError(AssertionError):
690- """Raised when an unknown expression is seen in playback mode."""
691-
692-
693-# --------------------------------------------------------------------
694-# Helper for chained-style calling.
695-
696-class expect(object):
697- """This is a simple helper that allows a different call-style.
698-
699- With this class one can comfortably do chaining of calls to the
700- mocker object responsible by the object being handled. For instance::
701-
702- expect(obj.attr).result(3).count(1, 2)
703-
704- Is the same as::
705-
706- obj.attr
707- mocker.result(3)
708- mocker.count(1, 2)
709-
710- """
711-
712- def __init__(self, mock, attr=None):
713- self._mock = mock
714- self._attr = attr
715-
716- def __getattr__(self, attr):
717- return self.__class__(self._mock, attr)
718-
719- def __call__(self, *args, **kwargs):
720- getattr(self._mock.__mocker__, self._attr)(*args, **kwargs)
721- return self
722-
723-
724-# --------------------------------------------------------------------
725-# Extensions to Python's unittest.
726-
727-class MockerTestCase(unittest.TestCase):
728- """unittest.TestCase subclass with Mocker support.
729-
730- @ivar mocker: The mocker instance.
731-
732- This is a convenience only. Mocker may easily be used with the
733- standard C{unittest.TestCase} class if wanted.
734-
735- Test methods have a Mocker instance available on C{self.mocker}.
736- At the end of each test method, expectations of the mocker will
737- be verified, and any requested changes made to the environment
738- will be restored.
739-
740- In addition to the integration with Mocker, this class provides
741- a few additional helper methods.
742- """
743-
744- expect = expect
745-
746- def __init__(self, methodName="runTest"):
747- # So here is the trick: we take the real test method, wrap it on
748- # a function that do the job we have to do, and insert it in the
749- # *instance* dictionary, so that getattr() will return our
750- # replacement rather than the class method.
751- test_method = getattr(self, methodName, None)
752- if test_method is not None:
753- def test_method_wrapper():
754- try:
755- result = test_method()
756- except:
757- raise
758- else:
759- if (self.mocker.is_recording() and
760- self.mocker.get_events()):
761- raise RuntimeError("Mocker must be put in replay "
762- "mode with self.mocker.replay()")
763- if (hasattr(result, "addCallback") and
764- hasattr(result, "addErrback")):
765- def verify(result):
766- self.mocker.verify()
767- return result
768- result.addCallback(verify)
769- else:
770- self.mocker.verify()
771- self.mocker.restore()
772- return result
773- # Copy all attributes from the original method..
774- for attr in dir(test_method):
775- # .. unless they're present in our wrapper already.
776- if not hasattr(test_method_wrapper, attr) or attr == "__doc__":
777- setattr(test_method_wrapper, attr,
778- getattr(test_method, attr))
779- setattr(self, methodName, test_method_wrapper)
780-
781- # We could overload run() normally, but other well-known testing
782- # frameworks do it as well, and some of them won't call the super,
783- # which might mean that cleanup wouldn't happen. With that in mind,
784- # we make integration easier by using the following trick.
785- run_method = self.run
786- def run_wrapper(*args, **kwargs):
787- try:
788- return run_method(*args, **kwargs)
789- finally:
790- self.__cleanup()
791- self.run = run_wrapper
792-
793- self.mocker = Mocker()
794-
795- self.__cleanup_funcs = []
796- self.__cleanup_paths = []
797-
798- super(MockerTestCase, self).__init__(methodName)
799-
800- def __cleanup(self):
801- for path in self.__cleanup_paths:
802- if os.path.isfile(path):
803- os.unlink(path)
804- elif os.path.isdir(path):
805- shutil.rmtree(path)
806- self.mocker.reset()
807- for func, args, kwargs in self.__cleanup_funcs:
808- func(*args, **kwargs)
809-
810- def addCleanup(self, func, *args, **kwargs):
811- self.__cleanup_funcs.append((func, args, kwargs))
812-
813- def makeFile(self, content=None, suffix="", prefix="tmp", basename=None,
814- dirname=None, path=None):
815- """Create a temporary file and return the path to it.
816-
817- @param content: Initial content for the file.
818- @param suffix: Suffix to be given to the file's basename.
819- @param prefix: Prefix to be given to the file's basename.
820- @param basename: Full basename for the file.
821- @param dirname: Put file inside this directory.
822-
823- The file is removed after the test runs.
824- """
825- if path is not None:
826- self.__cleanup_paths.append(path)
827- elif basename is not None:
828- if dirname is None:
829- dirname = tempfile.mkdtemp()
830- self.__cleanup_paths.append(dirname)
831- path = os.path.join(dirname, basename)
832- else:
833- fd, path = tempfile.mkstemp(suffix, prefix, dirname)
834- self.__cleanup_paths.append(path)
835- os.close(fd)
836- if content is None:
837- os.unlink(path)
838- if content is not None:
839- file = open(path, "w")
840- file.write(content)
841- file.close()
842- return path
843-
844- def makeDir(self, suffix="", prefix="tmp", dirname=None, path=None):
845- """Create a temporary directory and return the path to it.
846-
847- @param suffix: Suffix to be given to the file's basename.
848- @param prefix: Prefix to be given to the file's basename.
849- @param dirname: Put directory inside this parent directory.
850-
851- The directory is removed after the test runs.
852- """
853- if path is not None:
854- os.makedirs(path)
855- else:
856- path = tempfile.mkdtemp(suffix, prefix, dirname)
857- self.__cleanup_paths.append(path)
858- return path
859-
860- def failUnlessIs(self, first, second, msg=None):
861- """Assert that C{first} is the same object as C{second}."""
862- if first is not second:
863- raise self.failureException(msg or "%r is not %r" % (first, second))
864-
865- def failIfIs(self, first, second, msg=None):
866- """Assert that C{first} is not the same object as C{second}."""
867- if first is second:
868- raise self.failureException(msg or "%r is %r" % (first, second))
869-
870- def failUnlessIn(self, first, second, msg=None):
871- """Assert that C{first} is contained in C{second}."""
872- if first not in second:
873- raise self.failureException(msg or "%r not in %r" % (first, second))
874-
875- def failUnlessStartsWith(self, first, second, msg=None):
876- """Assert that C{first} starts with C{second}."""
877- if first[:len(second)] != second:
878- raise self.failureException(msg or "%r doesn't start with %r" %
879- (first, second))
880-
881- def failIfStartsWith(self, first, second, msg=None):
882- """Assert that C{first} doesn't start with C{second}."""
883- if first[:len(second)] == second:
884- raise self.failureException(msg or "%r starts with %r" %
885- (first, second))
886-
887- def failUnlessEndsWith(self, first, second, msg=None):
888- """Assert that C{first} starts with C{second}."""
889- if first[len(first)-len(second):] != second:
890- raise self.failureException(msg or "%r doesn't end with %r" %
891- (first, second))
892-
893- def failIfEndsWith(self, first, second, msg=None):
894- """Assert that C{first} doesn't start with C{second}."""
895- if first[len(first)-len(second):] == second:
896- raise self.failureException(msg or "%r ends with %r" %
897- (first, second))
898-
899- def failIfIn(self, first, second, msg=None):
900- """Assert that C{first} is not contained in C{second}."""
901- if first in second:
902- raise self.failureException(msg or "%r in %r" % (first, second))
903-
904- def failUnlessApproximates(self, first, second, tolerance, msg=None):
905- """Assert that C{first} is near C{second} by at most C{tolerance}."""
906- if abs(first - second) > tolerance:
907- raise self.failureException(msg or "abs(%r - %r) > %r" %
908- (first, second, tolerance))
909-
910- def failIfApproximates(self, first, second, tolerance, msg=None):
911- """Assert that C{first} is far from C{second} by at least C{tolerance}.
912- """
913- if abs(first - second) <= tolerance:
914- raise self.failureException(msg or "abs(%r - %r) <= %r" %
915- (first, second, tolerance))
916-
917- def failUnlessMethodsMatch(self, first, second):
918- """Assert that public methods in C{first} are present in C{second}.
919-
920- This method asserts that all public methods found in C{first} are also
921- present in C{second} and accept the same arguments. C{first} may
922- have its own private methods, though, and may not have all methods
923- found in C{second}. Note that if a private method in C{first} matches
924- the name of one in C{second}, their specification is still compared.
925-
926- This is useful to verify if a fake or stub class have the same API as
927- the real class being simulated.
928- """
929- first_methods = dict(inspect.getmembers(first, inspect.ismethod))
930- second_methods = dict(inspect.getmembers(second, inspect.ismethod))
931- for name, first_method in first_methods.iteritems():
932- first_argspec = inspect.getargspec(first_method)
933- first_formatted = inspect.formatargspec(*first_argspec)
934-
935- second_method = second_methods.get(name)
936- if second_method is None:
937- if name[:1] == "_":
938- continue # First may have its own private methods.
939- raise self.failureException("%s.%s%s not present in %s" %
940- (first.__name__, name, first_formatted, second.__name__))
941-
942- second_argspec = inspect.getargspec(second_method)
943- if first_argspec != second_argspec:
944- second_formatted = inspect.formatargspec(*second_argspec)
945- raise self.failureException("%s.%s%s != %s.%s%s" %
946- (first.__name__, name, first_formatted,
947- second.__name__, name, second_formatted))
948-
949-
950- assertIs = failUnlessIs
951- assertIsNot = failIfIs
952- assertIn = failUnlessIn
953- assertNotIn = failIfIn
954- assertStartsWith = failUnlessStartsWith
955- assertNotStartsWith = failIfStartsWith
956- assertEndsWith = failUnlessEndsWith
957- assertNotEndsWith = failIfEndsWith
958- assertApproximates = failUnlessApproximates
959- assertNotApproximates = failIfApproximates
960- assertMethodsMatch = failUnlessMethodsMatch
961-
962- # The following are missing in Python < 2.4.
963- assertTrue = unittest.TestCase.failUnless
964- assertFalse = unittest.TestCase.failIf
965-
966- # The following is provided for compatibility with Twisted's trial.
967- assertIdentical = assertIs
968- assertNotIdentical = assertIsNot
969- failUnlessIdentical = failUnlessIs
970- failIfIdentical = failIfIs
971-
972-
973-# --------------------------------------------------------------------
974-# Mocker.
975-
976-class classinstancemethod(object):
977-
978- def __init__(self, method):
979- self.method = method
980-
981- def __get__(self, obj, cls=None):
982- def bound_method(*args, **kwargs):
983- return self.method(cls, obj, *args, **kwargs)
984- return bound_method
985-
986-
987-class MockerBase(object):
988- """Controller of mock objects.
989-
990- A mocker instance is used to command recording and replay of
991- expectations on any number of mock objects.
992-
993- Expectations should be expressed for the mock object while in
994- record mode (the initial one) by using the mock object itself,
995- and using the mocker (and/or C{expect()} as a helper) to define
996- additional behavior for each event. For instance::
997-
998- mock = mocker.mock()
999- mock.hello()
1000- mocker.result("Hi!")
1001- mocker.replay()
1002- assert mock.hello() == "Hi!"
1003- mock.restore()
1004- mock.verify()
1005-
1006- In this short excerpt a mock object is being created, then an
1007- expectation of a call to the C{hello()} method was recorded, and
1008- when called the method should return the value C{10}. Then, the
1009- mocker is put in replay mode, and the expectation is satisfied by
1010- calling the C{hello()} method, which indeed returns 10. Finally,
1011- a call to the L{restore()} method is performed to undo any needed
1012- changes made in the environment, and the L{verify()} method is
1013- called to ensure that all defined expectations were met.
1014-
1015- The same logic can be expressed more elegantly using the
1016- C{with mocker:} statement, as follows::
1017-
1018- mock = mocker.mock()
1019- mock.hello()
1020- mocker.result("Hi!")
1021- with mocker:
1022- assert mock.hello() == "Hi!"
1023-
1024- Also, the MockerTestCase class, which integrates the mocker on
1025- a unittest.TestCase subclass, may be used to reduce the overhead
1026- of controlling the mocker. A test could be written as follows::
1027-
1028- class SampleTest(MockerTestCase):
1029-
1030- def test_hello(self):
1031- mock = self.mocker.mock()
1032- mock.hello()
1033- self.mocker.result("Hi!")
1034- self.mocker.replay()
1035- self.assertEquals(mock.hello(), "Hi!")
1036- """
1037-
1038- _recorders = []
1039-
1040- # For convenience only.
1041- on = expect
1042-
1043- class __metaclass__(type):
1044- def __init__(self, name, bases, dict):
1045- # Make independent lists on each subclass, inheriting from parent.
1046- self._recorders = list(getattr(self, "_recorders", ()))
1047-
1048- def __init__(self):
1049- self._recorders = self._recorders[:]
1050- self._events = []
1051- self._recording = True
1052- self._ordering = False
1053- self._last_orderer = None
1054-
1055- def is_recording(self):
1056- """Return True if in recording mode, False if in replay mode.
1057-
1058- Recording is the initial state.
1059- """
1060- return self._recording
1061-
1062- def replay(self):
1063- """Change to replay mode, where recorded events are reproduced.
1064-
1065- If already in replay mode, the mocker will be restored, with all
1066- expectations reset, and then put again in replay mode.
1067-
1068- An alternative and more comfortable way to replay changes is
1069- using the 'with' statement, as follows::
1070-
1071- mocker = Mocker()
1072- <record events>
1073- with mocker:
1074- <reproduce events>
1075-
1076- The 'with' statement will automatically put mocker in replay
1077- mode, and will also verify if all events were correctly reproduced
1078- at the end (using L{verify()}), and also restore any changes done
1079- in the environment (with L{restore()}).
1080-
1081- Also check the MockerTestCase class, which integrates the
1082- unittest.TestCase class with mocker.
1083- """
1084- if not self._recording:
1085- for event in self._events:
1086- event.restore()
1087- else:
1088- self._recording = False
1089- for event in self._events:
1090- event.replay()
1091-
1092- def restore(self):
1093- """Restore changes in the environment, and return to recording mode.
1094-
1095- This should always be called after the test is complete (succeeding
1096- or not). There are ways to call this method automatically on
1097- completion (e.g. using a C{with mocker:} statement, or using the
1098- L{MockerTestCase} class.
1099- """
1100- if not self._recording:
1101- self._recording = True
1102- for event in self._events:
1103- event.restore()
1104-
1105- def reset(self):
1106- """Reset the mocker state.
1107-
1108- This will restore environment changes, if currently in replay
1109- mode, and then remove all events previously recorded.
1110- """
1111- if not self._recording:
1112- self.restore()
1113- self.unorder()
1114- del self._events[:]
1115-
1116- def get_events(self):
1117- """Return all recorded events."""
1118- return self._events[:]
1119-
1120- def add_event(self, event):
1121- """Add an event.
1122-
1123- This method is used internally by the implementation, and
1124- shouldn't be needed on normal mocker usage.
1125- """
1126- self._events.append(event)
1127- if self._ordering:
1128- orderer = event.add_task(Orderer(event.path))
1129- if self._last_orderer:
1130- orderer.add_dependency(self._last_orderer)
1131- self._last_orderer = orderer
1132- return event
1133-
1134- def verify(self):
1135- """Check if all expectations were met, and raise AssertionError if not.
1136-
1137- The exception message will include a nice description of which
1138- expectations were not met, and why.
1139- """
1140- errors = []
1141- for event in self._events:
1142- try:
1143- event.verify()
1144- except AssertionError, e:
1145- error = str(e)
1146- if not error:
1147- raise RuntimeError("Empty error message from %r"
1148- % event)
1149- errors.append(error)
1150- if errors:
1151- message = [ERROR_PREFIX + "Unmet expectations:", ""]
1152- for error in errors:
1153- lines = error.splitlines()
1154- message.append("=> " + lines.pop(0))
1155- message.extend([" " + line for line in lines])
1156- message.append("")
1157- raise AssertionError(os.linesep.join(message))
1158-
1159- def mock(self, spec_and_type=None, spec=None, type=None,
1160- name=None, count=True):
1161- """Return a new mock object.
1162-
1163- @param spec_and_type: Handy positional argument which sets both
1164- spec and type.
1165- @param spec: Method calls will be checked for correctness against
1166- the given class.
1167- @param type: If set, the Mock's __class__ attribute will return
1168- the given type. This will make C{isinstance()} calls
1169- on the object work.
1170- @param name: Name for the mock object, used in the representation of
1171- expressions. The name is rarely needed, as it's usually
1172- guessed correctly from the variable name used.
1173- @param count: If set to false, expressions may be executed any number
1174- of times, unless an expectation is explicitly set using
1175- the L{count()} method. By default, expressions are
1176- expected once.
1177- """
1178- if spec_and_type is not None:
1179- spec = type = spec_and_type
1180- return Mock(self, spec=spec, type=type, name=name, count=count)
1181-
1182- def proxy(self, object, spec=True, type=True, name=None, count=True,
1183- passthrough=True):
1184- """Return a new mock object which proxies to the given object.
1185-
1186- Proxies are useful when only part of the behavior of an object
1187- is to be mocked. Unknown expressions may be passed through to
1188- the real implementation implicitly (if the C{passthrough} argument
1189- is True), or explicitly (using the L{passthrough()} method
1190- on the event).
1191-
1192- @param object: Real object to be proxied, and replaced by the mock
1193- on replay mode. It may also be an "import path",
1194- such as C{"time.time"}, in which case the object
1195- will be the C{time} function from the C{time} module.
1196- @param spec: Method calls will be checked for correctness against
1197- the given object, which may be a class or an instance
1198- where attributes will be looked up. Defaults to the
1199- the C{object} parameter. May be set to None explicitly,
1200- in which case spec checking is disabled. Checks may
1201- also be disabled explicitly on a per-event basis with
1202- the L{nospec()} method.
1203- @param type: If set, the Mock's __class__ attribute will return
1204- the given type. This will make C{isinstance()} calls
1205- on the object work. Defaults to the type of the
1206- C{object} parameter. May be set to None explicitly.
1207- @param name: Name for the mock object, used in the representation of
1208- expressions. The name is rarely needed, as it's usually
1209- guessed correctly from the variable name used.
1210- @param count: If set to false, expressions may be executed any number
1211- of times, unless an expectation is explicitly set using
1212- the L{count()} method. By default, expressions are
1213- expected once.
1214- @param passthrough: If set to False, passthrough of actions on the
1215- proxy to the real object will only happen when
1216- explicitly requested via the L{passthrough()}
1217- method.
1218- """
1219- if isinstance(object, basestring):
1220- if name is None:
1221- name = object
1222- import_stack = object.split(".")
1223- attr_stack = []
1224- while import_stack:
1225- module_path = ".".join(import_stack)
1226- try:
1227- object = __import__(module_path, {}, {}, [""])
1228- except ImportError:
1229- attr_stack.insert(0, import_stack.pop())
1230- if not import_stack:
1231- raise
1232- continue
1233- else:
1234- for attr in attr_stack:
1235- object = getattr(object, attr)
1236- break
1237- if spec is True:
1238- spec = object
1239- if type is True:
1240- type = __builtin__.type(object)
1241- return Mock(self, spec=spec, type=type, object=object,
1242- name=name, count=count, passthrough=passthrough)
1243-
1244- def replace(self, object, spec=True, type=True, name=None, count=True,
1245- passthrough=True):
1246- """Create a proxy, and replace the original object with the mock.
1247-
1248- On replay, the original object will be replaced by the returned
1249- proxy in all dictionaries found in the running interpreter via
1250- the garbage collecting system. This should cover module
1251- namespaces, class namespaces, instance namespaces, and so on.
1252-
1253- @param object: Real object to be proxied, and replaced by the mock
1254- on replay mode. It may also be an "import path",
1255- such as C{"time.time"}, in which case the object
1256- will be the C{time} function from the C{time} module.
1257- @param spec: Method calls will be checked for correctness against
1258- the given object, which may be a class or an instance
1259- where attributes will be looked up. Defaults to the
1260- the C{object} parameter. May be set to None explicitly,
1261- in which case spec checking is disabled. Checks may
1262- also be disabled explicitly on a per-event basis with
1263- the L{nospec()} method.
1264- @param type: If set, the Mock's __class__ attribute will return
1265- the given type. This will make C{isinstance()} calls
1266- on the object work. Defaults to the type of the
1267- C{object} parameter. May be set to None explicitly.
1268- @param name: Name for the mock object, used in the representation of
1269- expressions. The name is rarely needed, as it's usually
1270- guessed correctly from the variable name used.
1271- @param passthrough: If set to False, passthrough of actions on the
1272- proxy to the real object will only happen when
1273- explicitly requested via the L{passthrough()}
1274- method.
1275- """
1276- mock = self.proxy(object, spec, type, name, count, passthrough)
1277- event = self._get_replay_restore_event()
1278- event.add_task(ProxyReplacer(mock))
1279- return mock
1280-
1281- def patch(self, object, spec=True):
1282- """Patch an existing object to reproduce recorded events.
1283-
1284- @param object: Class or instance to be patched.
1285- @param spec: Method calls will be checked for correctness against
1286- the given object, which may be a class or an instance
1287- where attributes will be looked up. Defaults to the
1288- the C{object} parameter. May be set to None explicitly,
1289- in which case spec checking is disabled. Checks may
1290- also be disabled explicitly on a per-event basis with
1291- the L{nospec()} method.
1292-
1293- The result of this method is still a mock object, which can be
1294- used like any other mock object to record events. The difference
1295- is that when the mocker is put on replay mode, the *real* object
1296- will be modified to behave according to recorded expectations.
1297-
1298- Patching works in individual instances, and also in classes.
1299- When an instance is patched, recorded events will only be
1300- considered on this specific instance, and other instances should
1301- behave normally. When a class is patched, the reproduction of
1302- events will be considered on any instance of this class once
1303- created (collectively).
1304-
1305- Observe that, unlike with proxies which catch only events done
1306- through the mock object, *all* accesses to recorded expectations
1307- will be considered; even these coming from the object itself
1308- (e.g. C{self.hello()} is considered if this method was patched).
1309- While this is a very powerful feature, and many times the reason
1310- to use patches in the first place, it's important to keep this
1311- behavior in mind.
1312-
1313- Patching of the original object only takes place when the mocker
1314- is put on replay mode, and the patched object will be restored
1315- to its original state once the L{restore()} method is called
1316- (explicitly, or implicitly with alternative conventions, such as
1317- a C{with mocker:} block, or a MockerTestCase class).
1318- """
1319- if spec is True:
1320- spec = object
1321- patcher = Patcher()
1322- event = self._get_replay_restore_event()
1323- event.add_task(patcher)
1324- mock = Mock(self, object=object, patcher=patcher,
1325- passthrough=True, spec=spec)
1326- patcher.patch_attr(object, '__mocker_mock__', mock)
1327- return mock
1328-
1329- def act(self, path):
1330- """This is called by mock objects whenever something happens to them.
1331-
1332- This method is part of the implementation between the mocker
1333- and mock objects.
1334- """
1335- if self._recording:
1336- event = self.add_event(Event(path))
1337- for recorder in self._recorders:
1338- recorder(self, event)
1339- return Mock(self, path)
1340- else:
1341- # First run events that may run, then run unsatisfied events, then
1342- # ones not previously run. We put the index in the ordering tuple
1343- # instead of the actual event because we want a stable sort
1344- # (ordering between 2 events is undefined).
1345- events = self._events
1346- order = [(events[i].satisfied()*2 + events[i].has_run(), i)
1347- for i in range(len(events))]
1348- order.sort()
1349- postponed = None
1350- for weight, i in order:
1351- event = events[i]
1352- if event.matches(path):
1353- if event.may_run(path):
1354- return event.run(path)
1355- elif postponed is None:
1356- postponed = event
1357- if postponed is not None:
1358- return postponed.run(path)
1359- raise MatchError(ERROR_PREFIX + "Unexpected expression: %s" % path)
1360-
1361- def get_recorders(cls, self):
1362- """Return recorders associated with this mocker class or instance.
1363-
1364- This method may be called on mocker instances and also on mocker
1365- classes. See the L{add_recorder()} method for more information.
1366- """
1367- return (self or cls)._recorders[:]
1368- get_recorders = classinstancemethod(get_recorders)
1369-
1370- def add_recorder(cls, self, recorder):
1371- """Add a recorder to this mocker class or instance.
1372-
1373- @param recorder: Callable accepting C{(mocker, event)} as parameters.
1374-
1375- This is part of the implementation of mocker.
1376-
1377- All registered recorders are called for translating events that
1378- happen during recording into expectations to be met once the state
1379- is switched to replay mode.
1380-
1381- This method may be called on mocker instances and also on mocker
1382- classes. When called on a class, the recorder will be used by
1383- all instances, and also inherited on subclassing. When called on
1384- instances, the recorder is added only to the given instance.
1385- """
1386- (self or cls)._recorders.append(recorder)
1387- return recorder
1388- add_recorder = classinstancemethod(add_recorder)
1389-
1390- def remove_recorder(cls, self, recorder):
1391- """Remove the given recorder from this mocker class or instance.
1392-
1393- This method may be called on mocker classes and also on mocker
1394- instances. See the L{add_recorder()} method for more information.
1395- """
1396- (self or cls)._recorders.remove(recorder)
1397- remove_recorder = classinstancemethod(remove_recorder)
1398-
1399- def result(self, value):
1400- """Make the last recorded event return the given value on replay.
1401-
1402- @param value: Object to be returned when the event is replayed.
1403- """
1404- self.call(lambda *args, **kwargs: value)
1405-
1406- def generate(self, sequence):
1407- """Last recorded event will return a generator with the given sequence.
1408-
1409- @param sequence: Sequence of values to be generated.
1410- """
1411- def generate(*args, **kwargs):
1412- for value in sequence:
1413- yield value
1414- self.call(generate)
1415-
1416- def throw(self, exception):
1417- """Make the last recorded event raise the given exception on replay.
1418-
1419- @param exception: Class or instance of exception to be raised.
1420- """
1421- def raise_exception(*args, **kwargs):
1422- raise exception
1423- self.call(raise_exception)
1424-
1425- def call(self, func):
1426- """Make the last recorded event cause the given function to be called.
1427-
1428- @param func: Function to be called.
1429-
1430- The result of the function will be used as the event result.
1431- """
1432- self._events[-1].add_task(FunctionRunner(func))
1433-
1434- def count(self, min, max=False):
1435- """Last recorded event must be replayed between min and max times.
1436-
1437- @param min: Minimum number of times that the event must happen.
1438- @param max: Maximum number of times that the event must happen. If
1439- not given, it defaults to the same value of the C{min}
1440- parameter. If set to None, there is no upper limit, and
1441- the expectation is met as long as it happens at least
1442- C{min} times.
1443- """
1444- event = self._events[-1]
1445- for task in event.get_tasks():
1446- if isinstance(task, RunCounter):
1447- event.remove_task(task)
1448- event.add_task(RunCounter(min, max))
1449-
1450- def is_ordering(self):
1451- """Return true if all events are being ordered.
1452-
1453- See the L{order()} method.
1454- """
1455- return self._ordering
1456-
1457- def unorder(self):
1458- """Disable the ordered mode.
1459-
1460- See the L{order()} method for more information.
1461- """
1462- self._ordering = False
1463- self._last_orderer = None
1464-
1465- def order(self, *path_holders):
1466- """Create an expectation of order between two or more events.
1467-
1468- @param path_holders: Objects returned as the result of recorded events.
1469-
1470- By default, mocker won't force events to happen precisely in
1471- the order they were recorded. Calling this method will change
1472- this behavior so that events will only match if reproduced in
1473- the correct order.
1474-
1475- There are two ways in which this method may be used. Which one
1476- is used in a given occasion depends only on convenience.
1477-
1478- If no arguments are passed, the mocker will be put in a mode where
1479- all the recorded events following the method call will only be met
1480- if they happen in order. When that's used, the mocker may be put
1481- back in unordered mode by calling the L{unorder()} method, or by
1482- using a 'with' block, like so::
1483-
1484- with mocker.ordered():
1485- <record events>
1486-
1487- In this case, only expressions in <record events> will be ordered,
1488- and the mocker will be back in unordered mode after the 'with' block.
1489-
1490- The second way to use it is by specifying precisely which events
1491- should be ordered. As an example::
1492-
1493- mock = mocker.mock()
1494- expr1 = mock.hello()
1495- expr2 = mock.world
1496- expr3 = mock.x.y.z
1497- mocker.order(expr1, expr2, expr3)
1498-
1499- This method of ordering only works when the expression returns
1500- another object.
1501-
1502- Also check the L{after()} and L{before()} methods, which are
1503- alternative ways to perform this.
1504- """
1505- if not path_holders:
1506- self._ordering = True
1507- return OrderedContext(self)
1508-
1509- last_orderer = None
1510- for path_holder in path_holders:
1511- if type(path_holder) is Path:
1512- path = path_holder
1513- else:
1514- path = path_holder.__mocker_path__
1515- for event in self._events:
1516- if event.path is path:
1517- for task in event.get_tasks():
1518- if isinstance(task, Orderer):
1519- orderer = task
1520- break
1521- else:
1522- orderer = Orderer(path)
1523- event.add_task(orderer)
1524- if last_orderer:
1525- orderer.add_dependency(last_orderer)
1526- last_orderer = orderer
1527- break
1528-
1529- def after(self, *path_holders):
1530- """Last recorded event must happen after events referred to.
1531-
1532- @param path_holders: Objects returned as the result of recorded events
1533- which should happen before the last recorded event
1534-
1535- As an example, the idiom::
1536-
1537- expect(mock.x).after(mock.y, mock.z)
1538-
1539- is an alternative way to say::
1540-
1541- expr_x = mock.x
1542- expr_y = mock.y
1543- expr_z = mock.z
1544- mocker.order(expr_y, expr_x)
1545- mocker.order(expr_z, expr_x)
1546-
1547- See L{order()} for more information.
1548- """
1549- last_path = self._events[-1].path
1550- for path_holder in path_holders:
1551- self.order(path_holder, last_path)
1552-
1553- def before(self, *path_holders):
1554- """Last recorded event must happen before events referred to.
1555-
1556- @param path_holders: Objects returned as the result of recorded events
1557- which should happen after the last recorded event
1558-
1559- As an example, the idiom::
1560-
1561- expect(mock.x).before(mock.y, mock.z)
1562-
1563- is an alternative way to say::
1564-
1565- expr_x = mock.x
1566- expr_y = mock.y
1567- expr_z = mock.z
1568- mocker.order(expr_x, expr_y)
1569- mocker.order(expr_x, expr_z)
1570-
1571- See L{order()} for more information.
1572- """
1573- last_path = self._events[-1].path
1574- for path_holder in path_holders:
1575- self.order(last_path, path_holder)
1576-
1577- def nospec(self):
1578- """Don't check method specification of real object on last event.
1579-
1580- By default, when using a mock created as the result of a call to
1581- L{proxy()}, L{replace()}, and C{patch()}, or when passing the spec
1582- attribute to the L{mock()} method, method calls on the given object
1583- are checked for correctness against the specification of the real
1584- object (or the explicitly provided spec).
1585-
1586- This method will disable that check specifically for the last
1587- recorded event.
1588- """
1589- event = self._events[-1]
1590- for task in event.get_tasks():
1591- if isinstance(task, SpecChecker):
1592- event.remove_task(task)
1593-
1594- def passthrough(self, result_callback=None):
1595- """Make the last recorded event run on the real object once seen.
1596-
1597- @param result_callback: If given, this function will be called with
1598- the result of the *real* method call as the only argument.
1599-
1600- This can only be used on proxies, as returned by the L{proxy()}
1601- and L{replace()} methods, or on mocks representing patched objects,
1602- as returned by the L{patch()} method.
1603- """
1604- event = self._events[-1]
1605- if event.path.root_object is None:
1606- raise TypeError("Mock object isn't a proxy")
1607- event.add_task(PathExecuter(result_callback))
1608-
1609- def __enter__(self):
1610- """Enter in a 'with' context. This will run replay()."""
1611- self.replay()
1612- return self
1613-
1614- def __exit__(self, type, value, traceback):
1615- """Exit from a 'with' context.
1616-
1617- This will run restore() at all times, but will only run verify()
1618- if the 'with' block itself hasn't raised an exception. Exceptions
1619- in that block are never swallowed.
1620- """
1621- self.restore()
1622- if type is None:
1623- self.verify()
1624- return False
1625-
1626- def _get_replay_restore_event(self):
1627- """Return unique L{ReplayRestoreEvent}, creating if needed.
1628-
1629- Some tasks only want to replay/restore. When that's the case,
1630- they shouldn't act on other events during replay. Also, they
1631- can all be put in a single event when that's the case. Thus,
1632- we add a single L{ReplayRestoreEvent} as the first element of
1633- the list.
1634- """
1635- if not self._events or type(self._events[0]) != ReplayRestoreEvent:
1636- self._events.insert(0, ReplayRestoreEvent())
1637- return self._events[0]
1638-
1639-
1640-class OrderedContext(object):
1641-
1642- def __init__(self, mocker):
1643- self._mocker = mocker
1644-
1645- def __enter__(self):
1646- return None
1647-
1648- def __exit__(self, type, value, traceback):
1649- self._mocker.unorder()
1650-
1651-
1652-class Mocker(MockerBase):
1653- __doc__ = MockerBase.__doc__
1654-
1655-# Decorator to add recorders on the standard Mocker class.
1656-recorder = Mocker.add_recorder
1657-
1658-
1659-# --------------------------------------------------------------------
1660-# Mock object.
1661-
1662-class Mock(object):
1663-
1664- def __init__(self, mocker, path=None, name=None, spec=None, type=None,
1665- object=None, passthrough=False, patcher=None, count=True):
1666- self.__mocker__ = mocker
1667- self.__mocker_path__ = path or Path(self, object)
1668- self.__mocker_name__ = name
1669- self.__mocker_spec__ = spec
1670- self.__mocker_object__ = object
1671- self.__mocker_passthrough__ = passthrough
1672- self.__mocker_patcher__ = patcher
1673- self.__mocker_replace__ = False
1674- self.__mocker_type__ = type
1675- self.__mocker_count__ = count
1676-
1677- def __mocker_act__(self, kind, args=(), kwargs={}, object=None):
1678- if self.__mocker_name__ is None:
1679- self.__mocker_name__ = find_object_name(self, 2)
1680- action = Action(kind, args, kwargs, self.__mocker_path__)
1681- path = self.__mocker_path__ + action
1682- if object is not None:
1683- path.root_object = object
1684- try:
1685- return self.__mocker__.act(path)
1686- except MatchError, exception:
1687- root_mock = path.root_mock
1688- if (path.root_object is not None and
1689- root_mock.__mocker_passthrough__):
1690- return path.execute(path.root_object)
1691- # Reinstantiate to show raise statement on traceback, and
1692- # also to make the traceback shown shorter.
1693- raise MatchError(str(exception))
1694- except AssertionError, e:
1695- lines = str(e).splitlines()
1696- message = [ERROR_PREFIX + "Unmet expectation:", ""]
1697- message.append("=> " + lines.pop(0))
1698- message.extend([" " + line for line in lines])
1699- message.append("")
1700- raise AssertionError(os.linesep.join(message))
1701-
1702- def __getattribute__(self, name):
1703- if name.startswith("__mocker_"):
1704- return super(Mock, self).__getattribute__(name)
1705- if name == "__class__":
1706- if self.__mocker__.is_recording() or self.__mocker_type__ is None:
1707- return type(self)
1708- return self.__mocker_type__
1709- return self.__mocker_act__("getattr", (name,))
1710-
1711- def __setattr__(self, name, value):
1712- if name.startswith("__mocker_"):
1713- return super(Mock, self).__setattr__(name, value)
1714- return self.__mocker_act__("setattr", (name, value))
1715-
1716- def __delattr__(self, name):
1717- return self.__mocker_act__("delattr", (name,))
1718-
1719- def __call__(self, *args, **kwargs):
1720- return self.__mocker_act__("call", args, kwargs)
1721-
1722- def __contains__(self, value):
1723- return self.__mocker_act__("contains", (value,))
1724-
1725- def __getitem__(self, key):
1726- return self.__mocker_act__("getitem", (key,))
1727-
1728- def __setitem__(self, key, value):
1729- return self.__mocker_act__("setitem", (key, value))
1730-
1731- def __delitem__(self, key):
1732- return self.__mocker_act__("delitem", (key,))
1733-
1734- def __len__(self):
1735- # MatchError is turned on an AttributeError so that list() and
1736- # friends act properly when trying to get length hints on
1737- # something that doesn't offer them.
1738- try:
1739- result = self.__mocker_act__("len")
1740- except MatchError, e:
1741- raise AttributeError(str(e))
1742- if type(result) is Mock:
1743- return 0
1744- return result
1745-
1746- def __nonzero__(self):
1747- try:
1748- result = self.__mocker_act__("nonzero")
1749- except MatchError, e:
1750- return True
1751- if type(result) is Mock:
1752- return True
1753- return result
1754-
1755- def __iter__(self):
1756- # XXX On py3k, when next() becomes __next__(), we'll be able
1757- # to return the mock itself because it will be considered
1758- # an iterator (we'll be mocking __next__ as well, which we
1759- # can't now).
1760- result = self.__mocker_act__("iter")
1761- if type(result) is Mock:
1762- return iter([])
1763- return result
1764-
1765- # When adding a new action kind here, also add support for it on
1766- # Action.execute() and Path.__str__().
1767-
1768-
1769-def find_object_name(obj, depth=0):
1770- """Try to detect how the object is named on a previous scope."""
1771- try:
1772- frame = sys._getframe(depth+1)
1773- except:
1774- return None
1775- for name, frame_obj in frame.f_locals.iteritems():
1776- if frame_obj is obj:
1777- return name
1778- self = frame.f_locals.get("self")
1779- if self is not None:
1780- try:
1781- items = list(self.__dict__.iteritems())
1782- except:
1783- pass
1784- else:
1785- for name, self_obj in items:
1786- if self_obj is obj:
1787- return name
1788- return None
1789-
1790-
1791-# --------------------------------------------------------------------
1792-# Action and path.
1793-
1794-class Action(object):
1795-
1796- def __init__(self, kind, args, kwargs, path=None):
1797- self.kind = kind
1798- self.args = args
1799- self.kwargs = kwargs
1800- self.path = path
1801- self._execute_cache = {}
1802-
1803- def __repr__(self):
1804- if self.path is None:
1805- return "Action(%r, %r, %r)" % (self.kind, self.args, self.kwargs)
1806- return "Action(%r, %r, %r, %r)" % \
1807- (self.kind, self.args, self.kwargs, self.path)
1808-
1809- def __eq__(self, other):
1810- return (self.kind == other.kind and
1811- self.args == other.args and
1812- self.kwargs == other.kwargs)
1813-
1814- def __ne__(self, other):
1815- return not self.__eq__(other)
1816-
1817- def matches(self, other):
1818- return (self.kind == other.kind and
1819- match_params(self.args, self.kwargs, other.args, other.kwargs))
1820-
1821- def execute(self, object):
1822- # This caching scheme may fail if the object gets deallocated before
1823- # the action, as the id might get reused. It's somewhat easy to fix
1824- # that with a weakref callback. For our uses, though, the object
1825- # should never get deallocated before the action itself, so we'll
1826- # just keep it simple.
1827- if id(object) in self._execute_cache:
1828- return self._execute_cache[id(object)]
1829- execute = getattr(object, "__mocker_execute__", None)
1830- if execute is not None:
1831- result = execute(self, object)
1832- else:
1833- kind = self.kind
1834- if kind == "getattr":
1835- result = getattr(object, self.args[0])
1836- elif kind == "setattr":
1837- result = setattr(object, self.args[0], self.args[1])
1838- elif kind == "delattr":
1839- result = delattr(object, self.args[0])
1840- elif kind == "call":
1841- result = object(*self.args, **self.kwargs)
1842- elif kind == "contains":
1843- result = self.args[0] in object
1844- elif kind == "getitem":
1845- result = object[self.args[0]]
1846- elif kind == "setitem":
1847- result = object[self.args[0]] = self.args[1]
1848- elif kind == "delitem":
1849- del object[self.args[0]]
1850- result = None
1851- elif kind == "len":
1852- result = len(object)
1853- elif kind == "nonzero":
1854- result = bool(object)
1855- elif kind == "iter":
1856- result = iter(object)
1857- else:
1858- raise RuntimeError("Don't know how to execute %r kind." % kind)
1859- self._execute_cache[id(object)] = result
1860- return result
1861-
1862-
1863-class Path(object):
1864-
1865- def __init__(self, root_mock, root_object=None, actions=()):
1866- self.root_mock = root_mock
1867- self.root_object = root_object
1868- self.actions = tuple(actions)
1869- self.__mocker_replace__ = False
1870-
1871- def parent_path(self):
1872- if not self.actions:
1873- return None
1874- return self.actions[-1].path
1875- parent_path = property(parent_path)
1876-
1877- def __add__(self, action):
1878- """Return a new path which includes the given action at the end."""
1879- return self.__class__(self.root_mock, self.root_object,
1880- self.actions + (action,))
1881-
1882- def __eq__(self, other):
1883- """Verify if the two paths are equal.
1884-
1885- Two paths are equal if they refer to the same mock object, and
1886- have the actions with equal kind, args and kwargs.
1887- """
1888- if (self.root_mock is not other.root_mock or
1889- self.root_object is not other.root_object or
1890- len(self.actions) != len(other.actions)):
1891- return False
1892- for action, other_action in zip(self.actions, other.actions):
1893- if action != other_action:
1894- return False
1895- return True
1896-
1897- def matches(self, other):
1898- """Verify if the two paths are equivalent.
1899-
1900- Two paths are equal if they refer to the same mock object, and
1901- have the same actions performed on them.
1902- """
1903- if (self.root_mock is not other.root_mock or
1904- len(self.actions) != len(other.actions)):
1905- return False
1906- for action, other_action in zip(self.actions, other.actions):
1907- if not action.matches(other_action):
1908- return False
1909- return True
1910-
1911- def execute(self, object):
1912- """Execute all actions sequentially on object, and return result.
1913- """
1914- for action in self.actions:
1915- object = action.execute(object)
1916- return object
1917-
1918- def __str__(self):
1919- """Transform the path into a nice string such as obj.x.y('z')."""
1920- result = self.root_mock.__mocker_name__ or "<mock>"
1921- for action in self.actions:
1922- if action.kind == "getattr":
1923- result = "%s.%s" % (result, action.args[0])
1924- elif action.kind == "setattr":
1925- result = "%s.%s = %r" % (result, action.args[0], action.args[1])
1926- elif action.kind == "delattr":
1927- result = "del %s.%s" % (result, action.args[0])
1928- elif action.kind == "call":
1929- args = [repr(x) for x in action.args]
1930- items = list(action.kwargs.iteritems())
1931- items.sort()
1932- for pair in items:
1933- args.append("%s=%r" % pair)
1934- result = "%s(%s)" % (result, ", ".join(args))
1935- elif action.kind == "contains":
1936- result = "%r in %s" % (action.args[0], result)
1937- elif action.kind == "getitem":
1938- result = "%s[%r]" % (result, action.args[0])
1939- elif action.kind == "setitem":
1940- result = "%s[%r] = %r" % (result, action.args[0],
1941- action.args[1])
1942- elif action.kind == "delitem":
1943- result = "del %s[%r]" % (result, action.args[0])
1944- elif action.kind == "len":
1945- result = "len(%s)" % result
1946- elif action.kind == "nonzero":
1947- result = "bool(%s)" % result
1948- elif action.kind == "iter":
1949- result = "iter(%s)" % result
1950- else:
1951- raise RuntimeError("Don't know how to format kind %r" %
1952- action.kind)
1953- return result
1954-
1955-
1956-class SpecialArgument(object):
1957- """Base for special arguments for matching parameters."""
1958-
1959- def __init__(self, object=None):
1960- self.object = object
1961-
1962- def __repr__(self):
1963- if self.object is None:
1964- return self.__class__.__name__
1965- else:
1966- return "%s(%r)" % (self.__class__.__name__, self.object)
1967-
1968- def matches(self, other):
1969- return True
1970-
1971- def __eq__(self, other):
1972- return type(other) == type(self) and self.object == other.object
1973-
1974-
1975-class ANY(SpecialArgument):
1976- """Matches any single argument."""
1977-
1978-ANY = ANY()
1979-
1980-
1981-class ARGS(SpecialArgument):
1982- """Matches zero or more positional arguments."""
1983-
1984-ARGS = ARGS()
1985-
1986-
1987-class KWARGS(SpecialArgument):
1988- """Matches zero or more keyword arguments."""
1989-
1990-KWARGS = KWARGS()
1991-
1992-
1993-class IS(SpecialArgument):
1994-
1995- def matches(self, other):
1996- return self.object is other
1997-
1998- def __eq__(self, other):
1999- return type(other) == type(self) and self.object is other.object
2000-
2001-
2002-class CONTAINS(SpecialArgument):
2003-
2004- def matches(self, other):
2005- try:
2006- other.__contains__
2007- except AttributeError:
2008- try:
2009- iter(other)
2010- except TypeError:
2011- # If an object can't be iterated, and has no __contains__
2012- # hook, it'd blow up on the test below. We test this in
2013- # advance to prevent catching more errors than we really
2014- # want.
2015- return False
2016- return self.object in other
2017-
2018-
2019-class IN(SpecialArgument):
2020-
2021- def matches(self, other):
2022- return other in self.object
2023-
2024-
2025-class MATCH(SpecialArgument):
2026-
2027- def matches(self, other):
2028- return bool(self.object(other))
2029-
2030- def __eq__(self, other):
2031- return type(other) == type(self) and self.object is other.object
2032-
2033-
2034-def match_params(args1, kwargs1, args2, kwargs2):
2035- """Match the two sets of parameters, considering special parameters."""
2036-
2037- has_args = ARGS in args1
2038- has_kwargs = KWARGS in args1
2039-
2040- if has_kwargs:
2041- args1 = [arg1 for arg1 in args1 if arg1 is not KWARGS]
2042- elif len(kwargs1) != len(kwargs2):
2043- return False
2044-
2045- if not has_args and len(args1) != len(args2):
2046- return False
2047-
2048- # Either we have the same number of kwargs, or unknown keywords are
2049- # accepted (KWARGS was used), so check just the ones in kwargs1.
2050- for key, arg1 in kwargs1.iteritems():
2051- if key not in kwargs2:
2052- return False
2053- arg2 = kwargs2[key]
2054- if isinstance(arg1, SpecialArgument):
2055- if not arg1.matches(arg2):
2056- return False
2057- elif arg1 != arg2:
2058- return False
2059-
2060- # Keywords match. Now either we have the same number of
2061- # arguments, or ARGS was used. If ARGS wasn't used, arguments
2062- # must match one-on-one necessarily.
2063- if not has_args:
2064- for arg1, arg2 in zip(args1, args2):
2065- if isinstance(arg1, SpecialArgument):
2066- if not arg1.matches(arg2):
2067- return False
2068- elif arg1 != arg2:
2069- return False
2070- return True
2071-
2072- # Easy choice. Keywords are matching, and anything on args is accepted.
2073- if (ARGS,) == args1:
2074- return True
2075-
2076- # We have something different there. If we don't have positional
2077- # arguments on the original call, it can't match.
2078- if not args2:
2079- # Unless we have just several ARGS (which is bizarre, but..).
2080- for arg1 in args1:
2081- if arg1 is not ARGS:
2082- return False
2083- return True
2084-
2085- # Ok, all bets are lost. We have to actually do the more expensive
2086- # matching. This is an algorithm based on the idea of the Levenshtein
2087- # Distance between two strings, but heavily hacked for this purpose.
2088- args2l = len(args2)
2089- if args1[0] is ARGS:
2090- args1 = args1[1:]
2091- array = [0]*args2l
2092- else:
2093- array = [1]*args2l
2094- for i in range(len(args1)):
2095- last = array[0]
2096- if args1[i] is ARGS:
2097- for j in range(1, args2l):
2098- last, array[j] = array[j], min(array[j-1], array[j], last)
2099- else:
2100- array[0] = i or int(args1[i] != args2[0])
2101- for j in range(1, args2l):
2102- last, array[j] = array[j], last or int(args1[i] != args2[j])
2103- if 0 not in array:
2104- return False
2105- if array[-1] != 0:
2106- return False
2107- return True
2108-
2109-
2110-# --------------------------------------------------------------------
2111-# Event and task base.
2112-
2113-class Event(object):
2114- """Aggregation of tasks that keep track of a recorded action.
2115-
2116- An event represents something that may or may not happen while the
2117- mocked environment is running, such as an attribute access, or a
2118- method call. The event is composed of several tasks that are
2119- orchestrated together to create a composed meaning for the event,
2120- including for which actions it should be run, what happens when it
2121- runs, and what's the expectations about the actions run.
2122- """
2123-
2124- def __init__(self, path=None):
2125- self.path = path
2126- self._tasks = []
2127- self._has_run = False
2128-
2129- def add_task(self, task):
2130- """Add a new task to this taks."""
2131- self._tasks.append(task)
2132- return task
2133-
2134- def remove_task(self, task):
2135- self._tasks.remove(task)
2136-
2137- def get_tasks(self):
2138- return self._tasks[:]
2139-
2140- def matches(self, path):
2141- """Return true if *all* tasks match the given path."""
2142- for task in self._tasks:
2143- if not task.matches(path):
2144- return False
2145- return bool(self._tasks)
2146-
2147- def has_run(self):
2148- return self._has_run
2149-
2150- def may_run(self, path):
2151- """Verify if any task would certainly raise an error if run.
2152-
2153- This will call the C{may_run()} method on each task and return
2154- false if any of them returns false.
2155- """
2156- for task in self._tasks:
2157- if not task.may_run(path):
2158- return False
2159- return True
2160-
2161- def run(self, path):
2162- """Run all tasks with the given action.
2163-
2164- @param path: The path of the expression run.
2165-
2166- Running an event means running all of its tasks individually and in
2167- order. An event should only ever be run if all of its tasks claim to
2168- match the given action.
2169-
2170- The result of this method will be the last result of a task
2171- which isn't None, or None if they're all None.
2172- """
2173- self._has_run = True
2174- result = None
2175- errors = []
2176- for task in self._tasks:
2177- try:
2178- task_result = task.run(path)
2179- except AssertionError, e:
2180- error = str(e)
2181- if not error:
2182- raise RuntimeError("Empty error message from %r" % task)
2183- errors.append(error)
2184- else:
2185- if task_result is not None:
2186- result = task_result
2187- if errors:
2188- message = [str(self.path)]
2189- if str(path) != message[0]:
2190- message.append("- Run: %s" % path)
2191- for error in errors:
2192- lines = error.splitlines()
2193- message.append("- " + lines.pop(0))
2194- message.extend([" " + line for line in lines])
2195- raise AssertionError(os.linesep.join(message))
2196- return result
2197-
2198- def satisfied(self):
2199- """Return true if all tasks are satisfied.
2200-
2201- Being satisfied means that there are no unmet expectations.
2202- """
2203- for task in self._tasks:
2204- try:
2205- task.verify()
2206- except AssertionError:
2207- return False
2208- return True
2209-
2210- def verify(self):
2211- """Run verify on all tasks.
2212-
2213- The verify method is supposed to raise an AssertionError if the
2214- task has unmet expectations, with a one-line explanation about
2215- why this item is unmet. This method should be safe to be called
2216- multiple times without side effects.
2217- """
2218- errors = []
2219- for task in self._tasks:
2220- try:
2221- task.verify()
2222- except AssertionError, e:
2223- error = str(e)
2224- if not error:
2225- raise RuntimeError("Empty error message from %r" % task)
2226- errors.append(error)
2227- if errors:
2228- message = [str(self.path)]
2229- for error in errors:
2230- lines = error.splitlines()
2231- message.append("- " + lines.pop(0))
2232- message.extend([" " + line for line in lines])
2233- raise AssertionError(os.linesep.join(message))
2234-
2235- def replay(self):
2236- """Put all tasks in replay mode."""
2237- self._has_run = False
2238- for task in self._tasks:
2239- task.replay()
2240-
2241- def restore(self):
2242- """Restore the state of all tasks."""
2243- for task in self._tasks:
2244- task.restore()
2245-
2246-
2247-class ReplayRestoreEvent(Event):
2248- """Helper event for tasks which need replay/restore but shouldn't match."""
2249-
2250- def matches(self, path):
2251- return False
2252-
2253-
2254-class Task(object):
2255- """Element used to track one specific aspect on an event.
2256-
2257- A task is responsible for adding any kind of logic to an event.
2258- Examples of that are counting the number of times the event was
2259- made, verifying parameters if any, and so on.
2260- """
2261-
2262- def matches(self, path):
2263- """Return true if the task is supposed to be run for the given path.
2264- """
2265- return True
2266-
2267- def may_run(self, path):
2268- """Return false if running this task would certainly raise an error."""
2269- return True
2270-
2271- def run(self, path):
2272- """Perform the task item, considering that the given action happened.
2273- """
2274-
2275- def verify(self):
2276- """Raise AssertionError if expectations for this item are unmet.
2277-
2278- The verify method is supposed to raise an AssertionError if the
2279- task has unmet expectations, with a one-line explanation about
2280- why this item is unmet. This method should be safe to be called
2281- multiple times without side effects.
2282- """
2283-
2284- def replay(self):
2285- """Put the task in replay mode.
2286-
2287- Any expectations of the task should be reset.
2288- """
2289-
2290- def restore(self):
2291- """Restore any environmental changes made by the task.
2292-
2293- Verify should continue to work after this is called.
2294- """
2295-
2296-
2297-# --------------------------------------------------------------------
2298-# Task implementations.
2299-
2300-class OnRestoreCaller(Task):
2301- """Call a given callback when restoring."""
2302-
2303- def __init__(self, callback):
2304- self._callback = callback
2305-
2306- def restore(self):
2307- self._callback()
2308-
2309-
2310-class PathMatcher(Task):
2311- """Match the action path against a given path."""
2312-
2313- def __init__(self, path):
2314- self.path = path
2315-
2316- def matches(self, path):
2317- return self.path.matches(path)
2318-
2319-def path_matcher_recorder(mocker, event):
2320- event.add_task(PathMatcher(event.path))
2321-
2322-Mocker.add_recorder(path_matcher_recorder)
2323-
2324-
2325-class RunCounter(Task):
2326- """Task which verifies if the number of runs are within given boundaries.
2327- """
2328-
2329- def __init__(self, min, max=False):
2330- self.min = min
2331- if max is None:
2332- self.max = sys.maxint
2333- elif max is False:
2334- self.max = min
2335- else:
2336- self.max = max
2337- self._runs = 0
2338-
2339- def replay(self):
2340- self._runs = 0
2341-
2342- def may_run(self, path):
2343- return self._runs < self.max
2344-
2345- def run(self, path):
2346- self._runs += 1
2347- if self._runs > self.max:
2348- self.verify()
2349-
2350- def verify(self):
2351- if not self.min <= self._runs <= self.max:
2352- if self._runs < self.min:
2353- raise AssertionError("Performed fewer times than expected.")
2354- raise AssertionError("Performed more times than expected.")
2355-
2356-
2357-class ImplicitRunCounter(RunCounter):
2358- """RunCounter inserted by default on any event.
2359-
2360- This is a way to differentiate explicitly added counters and
2361- implicit ones.
2362- """
2363-
2364-def run_counter_recorder(mocker, event):
2365- """Any event may be repeated once, unless disabled by default."""
2366- if event.path.root_mock.__mocker_count__:
2367- event.add_task(ImplicitRunCounter(1))
2368-
2369-Mocker.add_recorder(run_counter_recorder)
2370-
2371-def run_counter_removal_recorder(mocker, event):
2372- """
2373- Events created by getattr actions which lead to other events
2374- may be repeated any number of times. For that, we remove implicit
2375- run counters of any getattr actions leading to the current one.
2376- """
2377- parent_path = event.path.parent_path
2378- for event in mocker.get_events()[::-1]:
2379- if (event.path is parent_path and
2380- event.path.actions[-1].kind == "getattr"):
2381- for task in event.get_tasks():
2382- if type(task) is ImplicitRunCounter:
2383- event.remove_task(task)
2384-
2385-Mocker.add_recorder(run_counter_removal_recorder)
2386-
2387-
2388-class MockReturner(Task):
2389- """Return a mock based on the action path."""
2390-
2391- def __init__(self, mocker):
2392- self.mocker = mocker
2393-
2394- def run(self, path):
2395- return Mock(self.mocker, path)
2396-
2397-def mock_returner_recorder(mocker, event):
2398- """Events that lead to other events must return mock objects."""
2399- parent_path = event.path.parent_path
2400- for event in mocker.get_events():
2401- if event.path is parent_path:
2402- for task in event.get_tasks():
2403- if isinstance(task, MockReturner):
2404- break
2405- else:
2406- event.add_task(MockReturner(mocker))
2407- break
2408-
2409-Mocker.add_recorder(mock_returner_recorder)
2410-
2411-
2412-class FunctionRunner(Task):
2413- """Task that runs a function everything it's run.
2414-
2415- Arguments of the last action in the path are passed to the function,
2416- and the function result is also returned.
2417- """
2418-
2419- def __init__(self, func):
2420- self._func = func
2421-
2422- def run(self, path):
2423- action = path.actions[-1]
2424- return self._func(*action.args, **action.kwargs)
2425-
2426-
2427-class PathExecuter(Task):
2428- """Task that executes a path in the real object, and returns the result."""
2429-
2430- def __init__(self, result_callback=None):
2431- self._result_callback = result_callback
2432-
2433- def get_result_callback(self):
2434- return self._result_callback
2435-
2436- def run(self, path):
2437- result = path.execute(path.root_object)
2438- if self._result_callback is not None:
2439- self._result_callback(result)
2440- return result
2441-
2442-
2443-class Orderer(Task):
2444- """Task to establish an order relation between two events.
2445-
2446- An orderer task will only match once all its dependencies have
2447- been run.
2448- """
2449-
2450- def __init__(self, path):
2451- self.path = path
2452- self._run = False
2453- self._dependencies = []
2454-
2455- def replay(self):
2456- self._run = False
2457-
2458- def has_run(self):
2459- return self._run
2460-
2461- def may_run(self, path):
2462- for dependency in self._dependencies:
2463- if not dependency.has_run():
2464- return False
2465- return True
2466-
2467- def run(self, path):
2468- for dependency in self._dependencies:
2469- if not dependency.has_run():
2470- raise AssertionError("Should be after: %s" % dependency.path)
2471- self._run = True
2472-
2473- def add_dependency(self, orderer):
2474- self._dependencies.append(orderer)
2475-
2476- def get_dependencies(self):
2477- return self._dependencies
2478-
2479-
2480-class SpecChecker(Task):
2481- """Task to check if arguments of the last action conform to a real method.
2482- """
2483-
2484- def __init__(self, method):
2485- self._method = method
2486- self._unsupported = False
2487-
2488- if method:
2489- try:
2490- self._args, self._varargs, self._varkwargs, self._defaults = \
2491- inspect.getargspec(method)
2492- except TypeError:
2493- self._unsupported = True
2494- else:
2495- if self._defaults is None:
2496- self._defaults = ()
2497- if type(method) is type(self.run):
2498- self._args = self._args[1:]
2499-
2500- def get_method(self):
2501- return self._method
2502-
2503- def _raise(self, message):
2504- spec = inspect.formatargspec(self._args, self._varargs,
2505- self._varkwargs, self._defaults)
2506- raise AssertionError("Specification is %s%s: %s" %
2507- (self._method.__name__, spec, message))
2508-
2509- def verify(self):
2510- if not self._method:
2511- raise AssertionError("Method not found in real specification")
2512-
2513- def may_run(self, path):
2514- try:
2515- self.run(path)
2516- except AssertionError:
2517- return False
2518- return True
2519-
2520- def run(self, path):
2521- if not self._method:
2522- raise AssertionError("Method not found in real specification")
2523- if self._unsupported:
2524- return # Can't check it. Happens with builtin functions. :-(
2525- action = path.actions[-1]
2526- obtained_len = len(action.args)
2527- obtained_kwargs = action.kwargs.copy()
2528- nodefaults_len = len(self._args) - len(self._defaults)
2529- for i, name in enumerate(self._args):
2530- if i < obtained_len and name in action.kwargs:
2531- self._raise("%r provided twice" % name)
2532- if (i >= obtained_len and i < nodefaults_len and
2533- name not in action.kwargs):
2534- self._raise("%r not provided" % name)
2535- obtained_kwargs.pop(name, None)
2536- if obtained_len > len(self._args) and not self._varargs:
2537- self._raise("too many args provided")
2538- if obtained_kwargs and not self._varkwargs:
2539- self._raise("unknown kwargs: %s" % ", ".join(obtained_kwargs))
2540-
2541-def spec_checker_recorder(mocker, event):
2542- spec = event.path.root_mock.__mocker_spec__
2543- if spec:
2544- actions = event.path.actions
2545- if len(actions) == 1:
2546- if actions[0].kind == "call":
2547- method = getattr(spec, "__call__", None)
2548- event.add_task(SpecChecker(method))
2549- elif len(actions) == 2:
2550- if actions[0].kind == "getattr" and actions[1].kind == "call":
2551- method = getattr(spec, actions[0].args[0], None)
2552- event.add_task(SpecChecker(method))
2553-
2554-Mocker.add_recorder(spec_checker_recorder)
2555-
2556-
2557-class ProxyReplacer(Task):
2558- """Task which installs and deinstalls proxy mocks.
2559-
2560- This task will replace a real object by a mock in all dictionaries
2561- found in the running interpreter via the garbage collecting system.
2562- """
2563-
2564- def __init__(self, mock):
2565- self.mock = mock
2566- self.__mocker_replace__ = False
2567-
2568- def replay(self):
2569- global_replace(self.mock.__mocker_object__, self.mock)
2570-
2571- def restore(self):
2572- global_replace(self.mock, self.mock.__mocker_object__)
2573-
2574-
2575-def global_replace(remove, install):
2576- """Replace object 'remove' with object 'install' on all dictionaries."""
2577- for referrer in gc.get_referrers(remove):
2578- if (type(referrer) is dict and
2579- referrer.get("__mocker_replace__", True)):
2580- for key, value in list(referrer.iteritems()):
2581- if value is remove:
2582- referrer[key] = install
2583-
2584-
2585-class Undefined(object):
2586-
2587- def __repr__(self):
2588- return "Undefined"
2589-
2590-Undefined = Undefined()
2591-
2592-
2593-class Patcher(Task):
2594-
2595- def __init__(self):
2596- super(Patcher, self).__init__()
2597- self._monitored = {} # {kind: {id(object): object}}
2598- self._patched = {}
2599-
2600- def is_monitoring(self, obj, kind):
2601- monitored = self._monitored.get(kind)
2602- if monitored:
2603- if id(obj) in monitored:
2604- return True
2605- cls = type(obj)
2606- if issubclass(cls, type):
2607- cls = obj
2608- bases = set([id(base) for base in cls.__mro__])
2609- bases.intersection_update(monitored)
2610- return bool(bases)
2611- return False
2612-
2613- def monitor(self, obj, kind):
2614- if kind not in self._monitored:
2615- self._monitored[kind] = {}
2616- self._monitored[kind][id(obj)] = obj
2617-
2618- def patch_attr(self, obj, attr, value):
2619- original = obj.__dict__.get(attr, Undefined)
2620- self._patched[id(obj), attr] = obj, attr, original
2621- setattr(obj, attr, value)
2622-
2623- def get_unpatched_attr(self, obj, attr):
2624- cls = type(obj)
2625- if issubclass(cls, type):
2626- cls = obj
2627- result = Undefined
2628- for mro_cls in cls.__mro__:
2629- key = (id(mro_cls), attr)
2630- if key in self._patched:
2631- result = self._patched[key][2]
2632- if result is not Undefined:
2633- break
2634- elif attr in mro_cls.__dict__:
2635- result = mro_cls.__dict__.get(attr, Undefined)
2636- break
2637- if isinstance(result, object) and hasattr(type(result), "__get__"):
2638- if cls is obj:
2639- obj = None
2640- return result.__get__(obj, cls)
2641- return result
2642-
2643- def _get_kind_attr(self, kind):
2644- if kind == "getattr":
2645- return "__getattribute__"
2646- return "__%s__" % kind
2647-
2648- def replay(self):
2649- for kind in self._monitored:
2650- attr = self._get_kind_attr(kind)
2651- seen = set()
2652- for obj in self._monitored[kind].itervalues():
2653- cls = type(obj)
2654- if issubclass(cls, type):
2655- cls = obj
2656- if cls not in seen:
2657- seen.add(cls)
2658- unpatched = getattr(cls, attr, Undefined)
2659- self.patch_attr(cls, attr,
2660- PatchedMethod(kind, unpatched,
2661- self.is_monitoring))
2662- self.patch_attr(cls, "__mocker_execute__",
2663- self.execute)
2664-
2665- def restore(self):
2666- for obj, attr, original in self._patched.itervalues():
2667- if original is Undefined:
2668- delattr(obj, attr)
2669- else:
2670- setattr(obj, attr, original)
2671- self._patched.clear()
2672-
2673- def execute(self, action, object):
2674- attr = self._get_kind_attr(action.kind)
2675- unpatched = self.get_unpatched_attr(object, attr)
2676- try:
2677- return unpatched(*action.args, **action.kwargs)
2678- except AttributeError:
2679- type, value, traceback = sys.exc_info()
2680- if action.kind == "getattr":
2681- # The normal behavior of Python is to try __getattribute__,
2682- # and if it raises AttributeError, try __getattr__. We've
2683- # tried the unpatched __getattribute__ above, and we'll now
2684- # try __getattr__.
2685- try:
2686- __getattr__ = unpatched("__getattr__")
2687- except AttributeError:
2688- pass
2689- else:
2690- return __getattr__(*action.args, **action.kwargs)
2691- raise type, value, traceback
2692-
2693-
2694-class PatchedMethod(object):
2695-
2696- def __init__(self, kind, unpatched, is_monitoring):
2697- self._kind = kind
2698- self._unpatched = unpatched
2699- self._is_monitoring = is_monitoring
2700-
2701- def __get__(self, obj, cls=None):
2702- object = obj or cls
2703- if not self._is_monitoring(object, self._kind):
2704- return self._unpatched.__get__(obj, cls)
2705- def method(*args, **kwargs):
2706- if self._kind == "getattr" and args[0].startswith("__mocker_"):
2707- return self._unpatched.__get__(obj, cls)(args[0])
2708- mock = object.__mocker_mock__
2709- return mock.__mocker_act__(self._kind, args, kwargs, object)
2710- return method
2711-
2712- def __call__(self, obj, *args, **kwargs):
2713- # At least with __getattribute__, Python seems to use *both* the
2714- # descriptor API and also call the class attribute directly. It
2715- # looks like an interpreter bug, or at least an undocumented
2716- # inconsistency.
2717- return self.__get__(obj)(*args, **kwargs)
2718-
2719-
2720-def patcher_recorder(mocker, event):
2721- mock = event.path.root_mock
2722- if mock.__mocker_patcher__ and len(event.path.actions) == 1:
2723- patcher = mock.__mocker_patcher__
2724- patcher.monitor(mock.__mocker_object__, event.path.actions[0].kind)
2725-
2726-Mocker.add_recorder(patcher_recorder)
2727
2728=== modified file 'setup.py'
2729--- setup.py 2010-08-19 16:02:09 +0000
2730+++ setup.py 2010-08-24 20:27:39 +0000
2731@@ -19,6 +19,15 @@
2732 import os
2733 import sys
2734
2735+try:
2736+ import DistUtilsExtra.auto
2737+except ImportError:
2738+ print >> sys.stderr, 'To build this program you need '\
2739+ 'https://launchpad.net/python-distutils-extra'
2740+ sys.exit(1)
2741+assert DistUtilsExtra.auto.__version__ >= '2.18', \
2742+ 'needs DistUtilsExtra.auto >= 2.18'
2743+
2744 from distutils.core import setup
2745 from distutils.spawn import find_executable
2746 from distutils.command import clean, build
2747@@ -70,9 +79,9 @@
2748 clean.clean.run(self)
2749
2750
2751-setup(
2752+DistUtilsExtra.auto.setup(
2753 name='ubuntu-sso-client',
2754- version='0.99.1',
2755+ version='0.99.2',
2756 license='GPL v3',
2757 author='Natalia Bidart',
2758 author_email='natalia.bidart@canonical.com',
2759@@ -93,5 +102,5 @@
2760 cmdclass = {
2761 'build' : SSOBuild,
2762 'clean' : SSOClean,
2763- },
2764- )
2765+ },
2766+)
2767
2768=== modified file 'ubuntu_sso/gui.py'
2769--- ubuntu_sso/gui.py 2010-08-19 16:02:09 +0000
2770+++ ubuntu_sso/gui.py 2010-08-24 20:27:39 +0000
2771@@ -39,6 +39,8 @@
2772
2773
2774 _ = gettext.gettext
2775+gettext.textdomain('ubuntu-sso-client')
2776+
2777 DBusGMainLoop(set_as_default=True)
2778 logger = setupLogging('ubuntu_sso.gui')
2779
2780@@ -57,11 +59,11 @@
2781 SIG_USER_CANCELATION = 'user-cancelation'
2782
2783 SIGNAL_ARGUMENTS = [
2784- (SIG_LOGIN_FAILED, ()),
2785- (SIG_LOGIN_SUCCEEDED, (gobject.TYPE_STRING,)),
2786- (SIG_REGISTRATION_FAILED, ()),
2787- (SIG_REGISTRATION_SUCCEEDED, (gobject.TYPE_STRING,)),
2788- (SIG_USER_CANCELATION, ()),
2789+ (SIG_LOGIN_FAILED, (gobject.TYPE_STRING, gobject.TYPE_STRING)),
2790+ (SIG_LOGIN_SUCCEEDED, (gobject.TYPE_STRING, gobject.TYPE_STRING,)),
2791+ (SIG_REGISTRATION_FAILED, (gobject.TYPE_STRING, gobject.TYPE_STRING)),
2792+ (SIG_REGISTRATION_SUCCEEDED, (gobject.TYPE_STRING, gobject.TYPE_STRING,)),
2793+ (SIG_USER_CANCELATION, (gobject.TYPE_STRING,)),
2794 ]
2795
2796 for sig, args in SIGNAL_ARGUMENTS:
2797@@ -100,6 +102,18 @@
2798 return os.path.join(get_data_dir(), filename)
2799
2800
2801+def log_call(f):
2802+ """Decorator to log call funtions."""
2803+
2804+ @wraps(f)
2805+ def inner(*args, **kwargs):
2806+ """Execute 'f' logging the call as INFO."""
2807+ logger.info('%s: args %r, kwargs %r.', f.__name__, args, kwargs)
2808+ return f(*args, **kwargs)
2809+
2810+ return inner
2811+
2812+
2813 class LabeledEntry(gtk.Entry):
2814 """An entry that displays the label within itself ina grey color."""
2815
2816@@ -175,9 +189,9 @@
2817 PASSWORD_CHANGED = _('Your password was successfully changed.')
2818 PASSWORD1_ENTRY = RESET_PASSWORD1_ENTRY = _('Password')
2819 PASSWORD2_ENTRY = RESET_PASSWORD2_ENTRY = _('Re-type Password')
2820- PASSWORD_HELP = _('The password must have a minimum of 8 characters and '
2821+ PASSWORD_HELP = _('The password must have a minimum of 8 characters and ' \
2822 'include one uppercase character and one number.')
2823- PASSWORD_MISMATCH = _('The passwords don\'t match, please double check '
2824+ PASSWORD_MISMATCH = _('The passwords don\'t match, please double check ' \
2825 'and try entering them again.')
2826 PASSWORD_TOO_WEAK = _('The password is too weak.')
2827 REQUEST_PASSWORD_TOKEN_LABEL = _('To reset your password enter your ' \
2828@@ -190,14 +204,14 @@
2829 'along with your new password.')
2830 SUCCESS = _('The process finished successfully. Congratulations!')
2831 TC = _('Terms & Conditions')
2832- TC_NOT_ACCEPTED = _('Agreeing to the Ubuntu One Terms & Conditions is '
2833+ TC_NOT_ACCEPTED = _('Agreeing to the Ubuntu One Terms & Conditions is ' \
2834 'required to subscribe.')
2835 UNKNOWN_ERROR = _('There was an error when trying to complete the ' \
2836 'process. Please check the information and try again.')
2837- VERIFY_EMAIL_LABEL = _("""Enter verification code.
2838-
2839-A verification code has just been sent to your email address.
2840-Please enter your code from the email. An example is shown below.""")
2841+ VERIFY_EMAIL_LABEL = _('Enter verification code.\n\nA verification code ' \
2842+ 'has just been sent to your email address.\n' \
2843+ 'Please enter your code from the email. ' \
2844+ 'An example is shown below.')
2845 YES_TO_TC = _('I agree with the %(app_name)s ') # Terms&Conditions button
2846 YES_TO_UPDATES = _('Yes! Email me %(app_name)s tips and updates.')
2847
2848@@ -295,6 +309,7 @@
2849 self._set_current_page(self.login_vbox)
2850
2851 self._setup_signals()
2852+ self._gtk_signal_log = []
2853
2854 if window_id != 0:
2855 # be as robust as possible:
2856@@ -471,7 +486,7 @@
2857 logger.info('Calling generate_captcha with filename path at %r',
2858 self._captcha_filename)
2859 f = self.backend.generate_captcha
2860- f(self._captcha_filename,
2861+ f(self.app_name, self._captcha_filename,
2862 reply_handler=NO_OP, error_handler=NO_OP)
2863
2864 return self.enter_details_vbox
2865@@ -563,9 +578,11 @@
2866 """Run the application."""
2867 gtk.main()
2868
2869- def connect(self, signal_name, handler):
2870+ def connect(self, signal_name, handler, *args, **kwargs):
2871 """Connect 'signal_name' with 'handler'."""
2872- self.window.connect(signal_name, handler)
2873+ logger.debug('connect: signal %r, handler %r, args %r, kwargs, %r',
2874+ signal_name, handler, args, kwargs)
2875+ self.window.connect(signal_name, handler, *args, **kwargs)
2876
2877 def on_close_clicked(self, *args, **kwargs):
2878 """Call self.close_callback if defined."""
2879@@ -583,7 +600,11 @@
2880 self.window.hide()
2881
2882 if len(args) > 0 and args[0] in self.cancels:
2883- self.window.emit(SIG_USER_CANCELATION)
2884+ self.window.emit(SIG_USER_CANCELATION, self.app_name)
2885+ elif len(self._gtk_signal_log) > 0:
2886+ signal = self._gtk_signal_log[-1][0]
2887+ args = self._gtk_signal_log[-1][1:]
2888+ self.window.emit(signal, *args)
2889
2890 # call user defined callback
2891 if self.close_callback is not None:
2892@@ -662,7 +683,7 @@
2893 ' captcha_id %r and captcha_solution %r.', email1,
2894 self._captcha_id, captcha_solution)
2895 f = self.backend.register_user
2896- f(email1, password1, self._captcha_id, captcha_solution,
2897+ f(self.app_name, email1, password1, self._captcha_id, captcha_solution,
2898 reply_handler=NO_OP, error_handler=NO_OP)
2899
2900 def on_tc_button_clicked(self, *args, **kwargs):
2901@@ -683,7 +704,7 @@
2902 ', app_name %r and email_token %r.', email,
2903 self.app_name, email_token)
2904 f = self.backend.validate_email
2905- f(email, password, self.app_name, email_token,
2906+ f(self.app_name, email, password, email_token,
2907 reply_handler=NO_OP, error_handler=NO_OP)
2908
2909 self._set_current_page(self.processing_vbox)
2910@@ -718,7 +739,7 @@
2911 self._clear_warnings()
2912
2913 f = self.backend.login
2914- f(email, password, self.app_name,
2915+ f(self.app_name, email, password,
2916 reply_handler=NO_OP, error_handler=NO_OP)
2917
2918 self._set_current_page(self.processing_vbox)
2919@@ -737,7 +758,7 @@
2920
2921 logger.info('Calling request_password_reset_token with %r.', email)
2922 f = self.backend.request_password_reset_token
2923- f(email, reply_handler=NO_OP, error_handler=NO_OP)
2924+ f(self.app_name, email, reply_handler=NO_OP, error_handler=NO_OP)
2925
2926 self._set_current_page(self.processing_vbox)
2927
2928@@ -771,7 +792,8 @@
2929 logger.info('Calling set_new_password with email %r, token %r and ' \
2930 'new password: <hidden>.', email, token)
2931 f = self.backend.set_new_password
2932- f(email, token, password1, reply_handler=NO_OP, error_handler=NO_OP)
2933+ f(self.app_name, email, token, password1,
2934+ reply_handler=NO_OP, error_handler=NO_OP)
2935
2936 self._set_current_page(self.processing_vbox)
2937
2938@@ -793,83 +815,81 @@
2939
2940 # backend callbacks
2941
2942- def on_captcha_generated(self, captcha_id, *args, **kwargs):
2943+ @log_call
2944+ def on_captcha_generated(self, app_name, captcha_id, *args, **kwargs):
2945 """Captcha image has been generated and is available to be shown."""
2946 assert captcha_id is not None
2947 self._captcha_id = captcha_id
2948 self._set_captcha_image()
2949
2950- def on_captcha_generation_error(self, *args, **kwargs):
2951+ @log_call
2952+ def on_captcha_generation_error(self, app_name, error, *args, **kwargs):
2953 """Captcha image generation failed."""
2954- logger.warning('on_captcha_generation_error: args %r, kwargs %r',
2955- args, kwargs)
2956
2957- def on_user_registered(self, *args, **kwargs):
2958+ @log_call
2959+ def on_user_registered(self, app_name, email, *args, **kwargs):
2960 """Registration can go on, user needs to verify email."""
2961- logger.info('on_user_registered: user was successfully registered! ' \
2962- 'args %r, kwargs %r.', args, kwargs)
2963 self._set_current_page(self.verify_email_vbox)
2964
2965- def on_user_registration_error(self, *args, **kwargs):
2966+ @log_call
2967+ def on_user_registration_error(self, app_name, error, *args, **kwargs):
2968 """Captcha image generation failed."""
2969- logger.warning('on_user_registration_error: args %r, kwargs %r',
2970- args, kwargs)
2971 self._set_current_page(self.enter_details_vbox,
2972 warning_text=self.UNKNOWN_ERROR)
2973- self.window.emit(SIG_REGISTRATION_FAILED)
2974+ self._gtk_signal_log.append((SIG_REGISTRATION_FAILED, self.app_name,
2975+ error))
2976
2977- def on_email_validated(self, app_name, *args, **kwargs):
2978+ @log_call
2979+ def on_email_validated(self, app_name, email, *args, **kwargs):
2980 """User email was successfully verified."""
2981- logger.info('on_email_validated: email was successfully validated! ' \
2982- 'app_name %r, args %r, kwargs %r.', app_name, args, kwargs)
2983 self._set_current_page(self.success_vbox)
2984- self.window.emit(SIG_REGISTRATION_SUCCEEDED, app_name)
2985+ self._gtk_signal_log.append((SIG_REGISTRATION_SUCCEEDED, self.app_name,
2986+ email))
2987
2988- def on_email_validation_error(self, *args, **kwargs):
2989+ @log_call
2990+ def on_email_validation_error(self, app_name, error, *args, **kwargs):
2991 """User email validation failed."""
2992- logger.warning('on_email_validation_error: args %r, kwargs %r',
2993- args, kwargs)
2994 self._set_current_page(self.verify_email_vbox,
2995 warning_text=self.UNKNOWN_ERROR)
2996- self.window.emit(SIG_REGISTRATION_FAILED)
2997+ self._gtk_signal_log.append((SIG_REGISTRATION_FAILED, self.app_name,
2998+ error))
2999
3000- def on_logged_in(self, app_name, *args, **kwargs):
3001+ @log_call
3002+ def on_logged_in(self, app_name, email, *args, **kwargs):
3003 """User was successfully logged in."""
3004- logger.info('on_logged_in: user was successfully logged in! '
3005- 'args %r, kwargs %r', args, kwargs)
3006 self._set_current_page(self.success_vbox)
3007- self.window.emit(SIG_LOGIN_SUCCEEDED, app_name)
3008+ self._gtk_signal_log.append((SIG_LOGIN_SUCCEEDED, self.app_name,
3009+ email))
3010
3011- def on_login_error(self, *args, **kwargs):
3012+ @log_call
3013+ def on_login_error(self, app_name, error, *args, **kwargs):
3014 """User was not successfully logged in."""
3015- logger.warning('on_login_error: args %r, kwargs %r',
3016- args, kwargs)
3017 self._set_current_page(self.login_vbox,
3018 warning_text=self.UNKNOWN_ERROR)
3019- self.window.emit(SIG_LOGIN_FAILED)
3020+ self._gtk_signal_log.append((SIG_LOGIN_FAILED, self.app_name,
3021+ error))
3022
3023- def on_password_reset_token_sent(self, email, *args, **kwargs):
3024+ @log_call
3025+ def on_password_reset_token_sent(self, app_name, email, *args, **kwargs):
3026 """Password reset token was successfully sent."""
3027- email = self.reset_email_entry.get_text()
3028 msg = self.SET_NEW_PASSWORD_LABEL % {'email': email}
3029 self.set_new_password_vbox.help_text = msg
3030 self._set_current_page(self.set_new_password_vbox)
3031
3032- def on_password_reset_error(self, *args, **kwargs):
3033+ @log_call
3034+ def on_password_reset_error(self, app_name, error, *args, **kwargs):
3035 """Password reset failed."""
3036- logger.warning('on_password_reset_error: args %r, kwargs %r',
3037- args, kwargs)
3038 self._set_current_page(self.login_vbox,
3039 warning_text=self.UNKNOWN_ERROR)
3040
3041- def on_password_changed(self, email, *args, **kwargs):
3042+ @log_call
3043+ def on_password_changed(self, app_name, email, *args, **kwargs):
3044 """Password was successfully changed."""
3045 self._set_current_page(self.login_vbox,
3046 warning_text=self.PASSWORD_CHANGED)
3047
3048- def on_password_change_error(self, *args, **kwargs):
3049+ @log_call
3050+ def on_password_change_error(self, app_name, error, *args, **kwargs):
3051 """Password reset failed."""
3052- logger.warning('on_password_change_error: args %r, kwargs %r',
3053- args, kwargs)
3054 self._set_current_page(self.request_password_token_vbox,
3055 warning_text=self.UNKNOWN_ERROR)
3056
3057=== modified file 'ubuntu_sso/main.py'
3058--- ubuntu_sso/main.py 2010-08-19 16:02:09 +0000
3059+++ ubuntu_sso/main.py 2010-08-24 20:27:39 +0000
3060@@ -31,6 +31,7 @@
3061 import socket
3062 import time
3063 import threading
3064+import traceback
3065 import urllib
3066 import urllib2
3067 import urlparse
3068@@ -45,10 +46,10 @@
3069 from lazr.restfulclient.resource import ServiceRoot
3070 from oauth.oauth import OAuthToken
3071
3072-from keyring import Keyring
3073 from ubuntu_sso import (DBUS_IFACE_AUTH_NAME, DBUS_IFACE_USER_NAME,
3074 DBUS_IFACE_CRED_NAME, DBUS_CRED_PATH, DBUS_BUS_NAME, gui)
3075 from ubuntu_sso.config import get_config
3076+from ubuntu_sso.keyring import Keyring
3077 from ubuntu_sso.logger import setupLogging
3078 logger = setupLogging("ubuntu_sso.main")
3079
3080@@ -56,6 +57,10 @@
3081 # Disable the invalid name warning, as we have a lot of DBus style names
3082 # pylint: disable-msg=C0103
3083
3084+OLD_KEY_NAME = "UbuntuOne token for https://ubuntuone.com"
3085+U1_APP_NAME = "Ubuntu One"
3086+PING_URL = "http://one.ubuntu.com/oauth/sso-finished-so-get-tokens/"
3087+
3088
3089 class NoDefaultConfigError(Exception):
3090 """No default section in configuration file"""
3091@@ -98,12 +103,32 @@
3092
3093 def keyring_store_credentials(app_name, credentials):
3094 """Store the credentials in the keyring."""
3095+ logger.debug('keyring_store_credentials: app_name %r.', app_name)
3096 Keyring(app_name).set_ubuntusso_attr(credentials)
3097
3098
3099 def keyring_get_credentials(app_name):
3100 """Get the credentials from the keyring or None if not there."""
3101- return Keyring(app_name).get_ubuntusso_attr()
3102+ logger.debug('keyring_get_credentials: app_name %r', app_name)
3103+ creds = Keyring(app_name).get_ubuntusso_attr()
3104+ logger.debug('keyring_get_credentials: Keyring returned credentials? %r',
3105+ creds is not None)
3106+ if creds is None and app_name == U1_APP_NAME:
3107+ logger.debug('keyring_get_credentials: trying for old service.')
3108+ # No new creds, try to get old credentials
3109+ old_creds = Keyring(OLD_KEY_NAME).get_ubuntusso_attr()
3110+ if old_creds is not None:
3111+ # Old creds found, build a new credentials dict with them
3112+ creds = {
3113+ 'consumer_key': "ubuntuone",
3114+ 'consumer_secret': "hammertime",
3115+ 'token_name': OLD_KEY_NAME,
3116+ 'token': old_creds["oauth_token"],
3117+ 'token_secret': old_creds["oauth_token_secret"],
3118+ }
3119+ else:
3120+ logger.debug('keyring_get_credentials: Keyring returned credentials.')
3121+ return creds
3122
3123
3124 def get_token_name(app_name):
3125@@ -182,17 +207,16 @@
3126 elif result['status'] != 'ok':
3127 raise RegistrationError('Received unknown status: %s' % result)
3128 else:
3129- return True
3130+ return email
3131
3132- def login(self, email, password, app_name):
3133+ def login(self, email, password, token_name):
3134 """Login a user with 'email' and 'password'."""
3135- logger.debug('login: email: %r password: <hidden>, app_name: %r',
3136- email, app_name)
3137+ logger.debug('login: email: %r password: <hidden>, token_name: %r',
3138+ email, token_name)
3139 basic = BasicHttpAuthorizer(email, password)
3140 sso_service = self.sso_service_class(basic, self.service_url)
3141 service = sso_service.authentications.authenticate
3142
3143- token_name = get_token_name(app_name)
3144 try:
3145 credentials = service(token_name=token_name)
3146 except HTTPError:
3147@@ -201,16 +225,15 @@
3148
3149 logger.debug('login: authentication successful! consumer_key: %r, ' \
3150 'token_name: %r', credentials['consumer_key'], token_name)
3151- keyring_store_credentials(app_name, credentials)
3152 return credentials
3153
3154- def validate_email(self, email, password, app_name, email_token):
3155+ def validate_email(self, email, password, email_token, token_name):
3156 """Validate an email token for user with 'email' and 'password'."""
3157 logger.debug('validate_email: email: %r password: <hidden>, '
3158- 'app_name: %r, email_token: %r',
3159- email, app_name, email_token)
3160+ 'email_token: %r, token_name: %r.',
3161+ email, email_token, token_name)
3162 token = self.login(email=email, password=password,
3163- app_name=app_name)
3164+ token_name=token_name)
3165
3166 oauth_token = OAuthToken(token['token'], token['token_secret'])
3167 authorizer = OAuthAuthorizer(token['consumer_key'],
3168@@ -222,7 +245,7 @@
3169 if 'errors' in result:
3170 raise EmailTokenError(result['errors'])
3171 elif 'email' in result:
3172- return True
3173+ return result['email']
3174 else:
3175 raise EmailTokenError('Received invalid reply: %s' % result)
3176
3177@@ -237,7 +260,7 @@
3178 raise ResetPasswordTokenError(e.content)
3179
3180 if result['status'] == 'ok':
3181- return True
3182+ return email
3183 else:
3184 raise ResetPasswordTokenError('Received invalid reply: %s' %
3185 result)
3186@@ -259,21 +282,29 @@
3187 raise NewPasswordError(e.content)
3188
3189 if result['status'] == 'ok':
3190- return True
3191+ return email
3192 else:
3193 raise NewPasswordError('Received invalid reply: %s' % result)
3194
3195
3196-def blocking(f, result_cb, error_cb):
3197+def except_to_errdict(e):
3198+ """Turn an exception into a dictionary to return thru DBus."""
3199+ return {
3200+ "errtype": type(e).__name__,
3201+ "message": "\n".join(str(arg) for arg in e.args),
3202+ }
3203+
3204+
3205+def blocking(f, app_name, result_cb, error_cb):
3206 """Run f in a thread; return or throw an exception thru the callbacks."""
3207 def _in_thread():
3208 """The part that runs inside the thread."""
3209 try:
3210- result_cb(f())
3211+ result_cb(app_name, f())
3212 except Exception, e:
3213 msg = "Exception while running DBus blocking code in a thread."""
3214 logger.exception(msg)
3215- error_cb(str(e))
3216+ error_cb(app_name, except_to_errdict(e))
3217 threading.Thread(target=_in_thread).start()
3218
3219
3220@@ -295,134 +326,142 @@
3221 sso_service_class=self.sso_service_class)
3222
3223 # generate_capcha signals
3224- @dbus.service.signal(DBUS_IFACE_USER_NAME, signature="s")
3225- def CaptchaGenerated(self, result):
3226+ @dbus.service.signal(DBUS_IFACE_USER_NAME, signature="ss")
3227+ def CaptchaGenerated(self, app_name, result):
3228 """Signal thrown after the captcha is generated."""
3229
3230- @dbus.service.signal(DBUS_IFACE_USER_NAME, signature="s")
3231- def CaptchaGenerationError(self, error):
3232+ @dbus.service.signal(DBUS_IFACE_USER_NAME, signature="sa{ss}")
3233+ def CaptchaGenerationError(self, app_name, error):
3234 """Signal thrown when there's a problem generating the captcha."""
3235
3236 @dbus.service.method(dbus_interface=DBUS_IFACE_USER_NAME,
3237- in_signature='s')
3238- def generate_captcha(self, filename):
3239+ in_signature='ss')
3240+ def generate_captcha(self, app_name, filename):
3241 """Call the matching method in the processor."""
3242 def f():
3243 """Inner function that will be run in a thread."""
3244 return self.processor().generate_captcha(filename)
3245- blocking(f, self.CaptchaGenerated, self.CaptchaGenerationError)
3246+ blocking(f, app_name, self.CaptchaGenerated,
3247+ self.CaptchaGenerationError)
3248
3249 # register_user signals
3250- @dbus.service.signal(DBUS_IFACE_USER_NAME, signature="b")
3251- def UserRegistered(self, result):
3252+ @dbus.service.signal(DBUS_IFACE_USER_NAME, signature="ss")
3253+ def UserRegistered(self, app_name, result):
3254 """Signal thrown when the user is registered."""
3255
3256- @dbus.service.signal(DBUS_IFACE_USER_NAME, signature="s")
3257- def UserRegistrationError(self, error):
3258+ @dbus.service.signal(DBUS_IFACE_USER_NAME, signature="sa{ss}")
3259+ def UserRegistrationError(self, app_name, error):
3260 """Signal thrown when there's a problem registering the user."""
3261
3262 @dbus.service.method(dbus_interface=DBUS_IFACE_USER_NAME,
3263- in_signature='ssss')
3264- def register_user(self, email, password, captcha_id, captcha_solution):
3265+ in_signature='sssss')
3266+ def register_user(self, app_name, email, password,
3267+ captcha_id, captcha_solution):
3268 """Call the matching method in the processor."""
3269 def f():
3270 """Inner function that will be run in a thread."""
3271 return self.processor().register_user(email, password,
3272- captcha_id, captcha_solution)
3273- blocking(f, self.UserRegistered, self.UserRegistrationError)
3274+ captcha_id, captcha_solution)
3275+ blocking(f, app_name, self.UserRegistered, self.UserRegistrationError)
3276
3277 # login signals
3278- @dbus.service.signal(DBUS_IFACE_USER_NAME, signature="s")
3279- def LoggedIn(self, result):
3280+ @dbus.service.signal(DBUS_IFACE_USER_NAME, signature="ss")
3281+ def LoggedIn(self, app_name, result):
3282 """Signal thrown when the user is logged in."""
3283
3284- @dbus.service.signal(DBUS_IFACE_USER_NAME, signature="s")
3285- def LoginError(self, error):
3286+ @dbus.service.signal(DBUS_IFACE_USER_NAME, signature="sa{ss}")
3287+ def LoginError(self, app_name, error):
3288 """Signal thrown when there is a problem in the login."""
3289
3290 @dbus.service.method(dbus_interface=DBUS_IFACE_USER_NAME,
3291 in_signature='sss')
3292- def login(self, email, password, app_name):
3293+ def login(self, app_name, email, password):
3294 """Call the matching method in the processor."""
3295 def f():
3296 """Inner function that will be run in a thread."""
3297- credentials = self.processor().login(email, password, app_name)
3298+ token_name = get_token_name(app_name)
3299+ logger.debug('login: token_name %r, email %r, password <hidden>.',
3300+ token_name, email)
3301+ credentials = self.processor().login(email, password, token_name)
3302+ logger.debug('login returned not None credentials? %r.',
3303+ credentials is not None)
3304 assert credentials is not None
3305- return app_name
3306- blocking(f, self.LoggedIn, self.LoginError)
3307+ keyring_store_credentials(app_name, credentials)
3308+ return email
3309+ blocking(f, app_name, self.LoggedIn, self.LoginError)
3310
3311 # validate_email signals
3312- @dbus.service.signal(DBUS_IFACE_USER_NAME, signature="s")
3313- def EmailValidated(self, app_name):
3314+ @dbus.service.signal(DBUS_IFACE_USER_NAME, signature="ss")
3315+ def EmailValidated(self, app_name, result):
3316 """Signal thrown after the email is validated."""
3317
3318- @dbus.service.signal(DBUS_IFACE_USER_NAME, signature="s")
3319- def EmailValidationError(self, error):
3320+ @dbus.service.signal(DBUS_IFACE_USER_NAME, signature="sa{ss}")
3321+ def EmailValidationError(self, app_name, error):
3322 """Signal thrown when there's a problem validating the email."""
3323
3324 @dbus.service.method(dbus_interface=DBUS_IFACE_USER_NAME,
3325 in_signature='ssss')
3326- def validate_email(self, email, password, app_name, email_token):
3327+ def validate_email(self, app_name, email, password, email_token):
3328 """Call the matching method in the processor."""
3329 def f():
3330 """Inner function that will be run in a thread."""
3331- self.processor().validate_email(email, password,
3332- app_name, email_token)
3333- return app_name
3334- blocking(f, self.EmailValidated, self.EmailValidationError)
3335+ token_name = get_token_name(app_name)
3336+ return self.processor().validate_email(email, password,
3337+ email_token, token_name)
3338+ blocking(f, app_name, self.EmailValidated, self.EmailValidationError)
3339
3340 # request_password_reset_token signals
3341- @dbus.service.signal(DBUS_IFACE_USER_NAME, signature="s")
3342- def PasswordResetTokenSent(self, email):
3343+ @dbus.service.signal(DBUS_IFACE_USER_NAME, signature="ss")
3344+ def PasswordResetTokenSent(self, app_name, email):
3345 """Signal thrown when the token is succesfully sent."""
3346
3347- @dbus.service.signal(DBUS_IFACE_USER_NAME, signature="s")
3348- def PasswordResetError(self, error):
3349+ @dbus.service.signal(DBUS_IFACE_USER_NAME, signature="sa{ss}")
3350+ def PasswordResetError(self, app_name, error):
3351 """Signal thrown when there's a problem sending the token."""
3352
3353 @dbus.service.method(dbus_interface=DBUS_IFACE_USER_NAME,
3354- in_signature='s')
3355- def request_password_reset_token(self, email):
3356+ in_signature='ss')
3357+ def request_password_reset_token(self, app_name, email):
3358 """Call the matching method in the processor."""
3359 def f():
3360 """Inner function that will be run in a thread."""
3361- self.processor().request_password_reset_token(email)
3362- return email
3363- blocking(f, self.PasswordResetTokenSent, self.PasswordResetError)
3364+ return self.processor().request_password_reset_token(email)
3365+ blocking(f, app_name, self.PasswordResetTokenSent,
3366+ self.PasswordResetError)
3367
3368 # set_new_password signals
3369- @dbus.service.signal(DBUS_IFACE_USER_NAME, signature="s")
3370- def PasswordChanged(self, email):
3371+ @dbus.service.signal(DBUS_IFACE_USER_NAME, signature="ss")
3372+ def PasswordChanged(self, app_name, email):
3373 """Signal thrown when the token is succesfully sent."""
3374
3375- @dbus.service.signal(DBUS_IFACE_USER_NAME, signature="s")
3376- def PasswordChangeError(self, error):
3377+ @dbus.service.signal(DBUS_IFACE_USER_NAME, signature="sa{ss}")
3378+ def PasswordChangeError(self, app_name, error):
3379 """Signal thrown when there's a problem sending the token."""
3380
3381 @dbus.service.method(dbus_interface=DBUS_IFACE_USER_NAME,
3382- in_signature='sss')
3383- def set_new_password(self, email, token, new_password):
3384+ in_signature='ssss')
3385+ def set_new_password(self, app_name, email, token, new_password):
3386 """Call the matching method in the processor."""
3387 def f():
3388 """Inner function that will be run in a thread."""
3389- self.processor().set_new_password(email, token, new_password)
3390- return email
3391- blocking(f, self.PasswordChanged, self.PasswordChangeError)
3392+ return self.processor().set_new_password(email, token,
3393+ new_password)
3394+ blocking(f, app_name, self.PasswordChanged, self.PasswordChangeError)
3395
3396
3397 class SSOCredentials(dbus.service.Object):
3398 """DBus object that gets credentials, and login/registers if needed."""
3399
3400- @dbus.service.signal(DBUS_IFACE_CRED_NAME, signature="")
3401- def AuthorizationDenied(self):
3402+ @dbus.service.signal(DBUS_IFACE_CRED_NAME, signature="s")
3403+ def AuthorizationDenied(self, app_name):
3404 """Signal thrown when the user denies the authorization."""
3405
3406- @dbus.service.signal(DBUS_IFACE_CRED_NAME, signature="a{ss}")
3407- def CredentialsFound(self, app_name):
3408+ @dbus.service.signal(DBUS_IFACE_CRED_NAME, signature="sa{ss}")
3409+ def CredentialsFound(self, app_name, credentials):
3410 """Signal thrown when the credentials are found."""
3411
3412- @dbus.service.signal(DBUS_IFACE_CRED_NAME, signature="s")
3413- def CredentialsError(self, error):
3414+ @dbus.service.signal(DBUS_IFACE_CRED_NAME, signature="sss")
3415+ def CredentialsError(self, app_name, error_message, detailed_error):
3416 """Signal thrown when there is a problem finding the credentials."""
3417
3418 @dbus.service.method(dbus_interface=DBUS_IFACE_CRED_NAME,
3419@@ -435,24 +474,31 @@
3420 else:
3421 return token
3422
3423- def _login_success_cb(self, dialog, app_name):
3424+ def _login_success_cb(self, dialog, app_name, email):
3425 """Handles the response from the UI dialog."""
3426+ logger.info('Login successful for app %r, email %r', app_name, email)
3427 try:
3428 creds = keyring_get_credentials(app_name)
3429- self.CredentialsFound(creds)
3430- except Exception, e:
3431- logger.exception("problem when getting credentials from keyring")
3432- self.CredentialsError(str(e))
3433+ self.CredentialsFound(app_name, creds)
3434+ except Exception:
3435+ msg = "Problem getting the credentials from the keyring."
3436+ logger.exception(msg)
3437+ self.CredentialsError(app_name, msg, traceback.format_exc())
3438
3439- def _login_error_cb(self, dialog):
3440+ def _login_error_cb(self, dialog, app_name, error):
3441 """Handles a problem in the UI."""
3442- msg = "misc problem getting the credentials"
3443- self.CredentialsError(msg)
3444- logger.error(msg)
3445+ logger.info('Login unsuccessful for app %r, error %r', app_name, error)
3446+ msg = "Problem getting the credentials from the keyring."
3447+ self.CredentialsError(app_name, msg, "no more info available")
3448
3449- def _login_auth_denied_cb(self, dialog):
3450+ def _login_auth_denied_cb(self, dialog, app_name):
3451 """The user decides not to allow the registration or login."""
3452- self.AuthorizationDenied()
3453+ self.AuthorizationDenied(app_name)
3454+
3455+ def _ping_url(self, dialog, app_name, email):
3456+ """Ping the given url."""
3457+ if app_name == U1_APP_NAME:
3458+ urllib2.urlopen(PING_URL + email)
3459
3460 def _show_login_or_register_ui(self, app_name, tc_url, help_text,
3461 win_id, login_only=False):
3462@@ -461,18 +507,21 @@
3463 gui_app = gui.UbuntuSSOClientGUI(app_name, tc_url,
3464 help_text, win_id, login_only)
3465 gui_app.connect(gui.SIG_LOGIN_SUCCEEDED, self._login_success_cb)
3466+ gui_app.connect(gui.SIG_LOGIN_SUCCEEDED, self._ping_url)
3467 gui_app.connect(gui.SIG_LOGIN_FAILED, self._login_error_cb)
3468 gui_app.connect(gui.SIG_REGISTRATION_SUCCEEDED,
3469 self._login_success_cb)
3470+ gui_app.connect(gui.SIG_REGISTRATION_SUCCEEDED, self._ping_url)
3471 gui_app.connect(gui.SIG_REGISTRATION_FAILED, self._login_error_cb)
3472 gui_app.connect(gui.SIG_USER_CANCELATION,
3473 self._login_auth_denied_cb)
3474- except Exception, e:
3475+ except Exception:
3476 msg = '_show_login_or_register_ui failed when calling ' \
3477 'gui.UbuntuSSOClientGUI(%r, %r, %r, %r, %r)'
3478 logger.exception(msg, app_name, tc_url, help_text,
3479 win_id, login_only)
3480- self.CredentialsError(str(e))
3481+ msg = "Problem opening the Ubuntu SSO user interface."
3482+ self.CredentialsError(app_name, msg, traceback.format_exc())
3483
3484 @dbus.service.method(dbus_interface=DBUS_IFACE_CRED_NAME,
3485 in_signature="sssx", out_signature="")
3486@@ -496,10 +545,11 @@
3487 app_name, terms_and_conditions_url,
3488 help_text, window_id)
3489 else:
3490- self.CredentialsFound(token)
3491- except Exception, e:
3492- logger.exception("problem while getting the credentials")
3493- self.CredentialsError(str(e))
3494+ self.CredentialsFound(app_name, token)
3495+ except Exception:
3496+ msg = "Problem getting the credentials from the keyring."
3497+ logger.exception(msg)
3498+ self.CredentialsError(app_name, msg, traceback.format_exc())
3499
3500 def _show_login_only_ui(self, app_name, help_text, win_id):
3501 """Shows the UI so the user can login."""
3502@@ -525,10 +575,26 @@
3503 gobject.idle_add(self._show_login_only_ui, app_name,
3504 help_text, window_id)
3505 else:
3506- self.CredentialsFound(token)
3507- except Exception, e:
3508- logger.exception("problem while getting the credentials")
3509- self.CredentialsError(str(e))
3510+ self.CredentialsFound(app_name, token)
3511+ except Exception:
3512+ msg = "Problem getting the credentials from the keyring."
3513+ logger.exception(msg)
3514+ self.CredentialsError(app_name, msg, traceback.format_exc())
3515+
3516+ @dbus.service.method(dbus_interface=DBUS_IFACE_CRED_NAME,
3517+ in_signature='s', out_signature='')
3518+ def clear_token(self, app_name):
3519+ """Clear the token for an application from the keyring.
3520+
3521+ 'app_name' is the name of the application.
3522+ """
3523+ try:
3524+ creds = Keyring(app_name)
3525+ creds.delete_ubuntusso_attr()
3526+ except Exception:
3527+ logger.exception(
3528+ "problem removing credentials from keyring for %s",
3529+ app_name)
3530
3531
3532 class LoginProcessor:
3533
3534=== modified file 'ubuntu_sso/tests/test_gui.py'
3535--- ubuntu_sso/tests/test_gui.py 2010-08-19 16:02:09 +0000
3536+++ ubuntu_sso/tests/test_gui.py 2010-08-24 20:27:39 +0000
3537@@ -44,6 +44,7 @@
3538 CAPTCHA_SOLUTION = 'william Byrd'
3539 EMAIL = 'test@example.com'
3540 EMAIL_TOKEN = 'B2Pgtf'
3541+ERROR = 'Something bad happened!'
3542 NAME = 'Juanito Pérez'
3543 PASSWORD = 'h3lloWorld'
3544 RESET_PASSWORD_TOKEN = '8G5Wtq'
3545@@ -358,7 +359,7 @@
3546
3547 def click_join_with_valid_data(self):
3548 """Move to the next page after entering details."""
3549- self.ui.on_captcha_generated(captcha_id=CAPTCHA_ID) # we have captcha!
3550+ self.ui.on_captcha_generated(app_name=APP_NAME, captcha_id=CAPTCHA_ID)
3551
3552 self.ui.name_entry.set_text(NAME)
3553 # match emails
3554@@ -520,18 +521,6 @@
3555 """Main window has the proper icon."""
3556 self.assertEqual('ubuntu-logo', self.ui.window.get_icon_name())
3557
3558- def test_every_cancel_emits_proper_signal(self):
3559- """Clicking on any cancel button, 'user-cancelation' signal is sent."""
3560- msg = 'user-cancelation is emitted when "%s" is clicked.'
3561- buttons = filter(lambda name: 'cancel_button' in name, self.ui.widgets)
3562- for name in buttons:
3563- self.ui = self.gui_class(**self.kwargs)
3564- self.ui.connect(gui.SIG_USER_CANCELATION, self._set_called)
3565- widget = getattr(self.ui, name)
3566- widget.clicked()
3567- self.assertEqual(self._called, ((self.ui.window,), {}), msg % name)
3568- self._called = False
3569-
3570 def test_transient_window_is_None_if_window_id_is_zero(self):
3571 """The transient window is correct."""
3572 self.patch(gtk.gdk, 'window_foreign_new', self._set_called)
3573@@ -610,7 +599,8 @@
3574 expected = 'register_user'
3575 self.assertIn(expected, self.ui.backend._called)
3576 self.assertEqual(self.ui.backend._called[expected],
3577- ((EMAIL, PASSWORD, CAPTCHA_ID, CAPTCHA_SOLUTION),
3578+ ((APP_NAME, EMAIL, PASSWORD, CAPTCHA_ID,
3579+ CAPTCHA_SOLUTION),
3580 dict(reply_handler=gui.NO_OP,
3581 error_handler=gui.NO_OP)))
3582
3583@@ -690,18 +680,18 @@
3584
3585 def test_join_ok_button_is_enabled_when_captcha_is_available(self):
3586 """The join_ok_button is sensitive when captcha is available."""
3587- self.ui.on_captcha_generated(captcha_id=CAPTCHA_ID)
3588+ self.ui.on_captcha_generated(app_name=APP_NAME, captcha_id=CAPTCHA_ID)
3589 self.assertTrue(self.ui.join_ok_button.is_sensitive())
3590
3591 def test_captcha_loading_is_hid_when_captcha_is_available(self):
3592 """The captcha_loading is hid when captcha is available."""
3593- self.ui.on_captcha_generated(captcha_id=CAPTCHA_ID)
3594+ self.ui.on_captcha_generated(app_name=APP_NAME, captcha_id=CAPTCHA_ID)
3595 self.assertFalse(self.ui.captcha_loading.get_property('visible'),
3596 'captcha_loading is not visible.')
3597
3598 def test_captcha_id_is_stored_when_captcha_is_available(self):
3599 """The captcha_id is stored when captcha is available."""
3600- self.ui.on_captcha_generated(captcha_id=CAPTCHA_ID)
3601+ self.ui.on_captcha_generated(app_name=APP_NAME, captcha_id=CAPTCHA_ID)
3602 self.assertEqual(CAPTCHA_ID, self.ui._captcha_id)
3603
3604 def test_captcha_image_is_requested_as_startup(self):
3605@@ -710,14 +700,14 @@
3606 expected = 'generate_captcha'
3607 self.assertIn(expected, self.ui.backend._called)
3608 self.assertEqual(self.ui.backend._called[expected],
3609- ((self.ui._captcha_filename,),
3610+ ((APP_NAME, self.ui._captcha_filename),
3611 dict(reply_handler=gui.NO_OP,
3612 error_handler=gui.NO_OP)))
3613
3614 def test_captcha_is_shown_when_available(self):
3615 """The captcha image is shown when available."""
3616 self.patch(self.ui.captcha_image, 'set_from_file', self._set_called)
3617- self.ui.on_captcha_generated(captcha_id=CAPTCHA_ID)
3618+ self.ui.on_captcha_generated(app_name=APP_NAME, captcha_id=CAPTCHA_ID)
3619 self.assertTrue(self.ui.captcha_image.get_property('visible'))
3620 self.assertEqual(self._called, ((self.ui._captcha_filename,), {}))
3621
3622@@ -782,7 +772,7 @@
3623 """Init."""
3624 super(UserRegistrationErrorTestCase, self).setUp()
3625 self.click_join_with_valid_data()
3626- self.ui.on_user_registration_error()
3627+ self.ui.on_user_registration_error(app_name=APP_NAME, error=ERROR)
3628
3629 def test_previous_page_is_shown(self):
3630 """On UserRegistrationError the previous page is shown."""
3631@@ -793,12 +783,6 @@
3632 self.assert_correct_warning(self.ui.warning_label,
3633 self.ui.UNKNOWN_ERROR)
3634
3635- def test_proper_signal_is_emitted(self):
3636- """On UserRegistrationError, 'registration-failed' signal is sent."""
3637- self.ui.connect(gui.SIG_REGISTRATION_FAILED, self._set_called)
3638- self.ui.on_user_registration_error()
3639- self.assertEqual(((self.ui.window,), {}), self._called)
3640-
3641
3642 class VerifyEmailTestCase(UbuntuSSOClientTestCase):
3643 """Test suite for the user registration (verify email page)."""
3644@@ -806,6 +790,7 @@
3645 def setUp(self):
3646 """Init."""
3647 super(VerifyEmailTestCase, self).setUp()
3648+ self.ui.on_user_registered(app_name=APP_NAME, email=EMAIL)
3649 self.click_verify_email_with_valid_data()
3650
3651 def test_verify_email_image_is_correct(self):
3652@@ -816,12 +801,11 @@
3653
3654 def test_registration_successful_shows_verify_email_vbox(self):
3655 """Receiving 'registration_successful' shows the verify email vbox."""
3656- self.ui.on_user_registered()
3657+ self.ui.on_user_registered(app_name=APP_NAME, email=EMAIL)
3658 self.assert_pages_visibility(verify_email=True)
3659
3660 def test_help_label_display_correct_wording(self):
3661 """The help_label display VERIFY_EMAIL_LABEL."""
3662- self.ui.on_user_registered()
3663 msg = 'help_label must read "%s" (got "%s" instead).'
3664 actual = self.ui.help_label.get_text()
3665 expected = self.ui.VERIFY_EMAIL_LABEL
3666@@ -833,7 +817,7 @@
3667 expected = 'validate_email'
3668 self.assertIn(expected, self.ui.backend._called)
3669 self.assertEqual(self.ui.backend._called[expected],
3670- ((EMAIL, PASSWORD, APP_NAME, EMAIL_TOKEN),
3671+ ((APP_NAME, EMAIL, PASSWORD, EMAIL_TOKEN),
3672 dict(reply_handler=gui.NO_OP,
3673 error_handler=gui.NO_OP)))
3674
3675@@ -850,34 +834,22 @@
3676
3677 def test_on_email_validated_shows_success_page(self):
3678 """On email validated the success page is shown."""
3679- self.ui.on_email_validated(APP_NAME)
3680+ self.ui.on_email_validated(app_name=APP_NAME, email=EMAIL)
3681 self.assert_pages_visibility(success=True)
3682
3683 def test_on_email_validated_clears_the_help_text(self):
3684 """On email validated the help text is removed."""
3685- self.ui.on_email_validated(APP_NAME)
3686+ self.ui.on_email_validated(app_name=APP_NAME, email=EMAIL)
3687 self.assertEqual('', self.ui.help_label.get_text())
3688 test_on_email_validated_clears_the_help_text.skip = 'Maybe this is wrong.'
3689
3690- def test_on_email_validated_proper_signals_is_emitted(self):
3691- """On email validated, 'registration-succeeded' signal is sent."""
3692- self.ui.connect(gui.SIG_REGISTRATION_SUCCEEDED, self._set_called)
3693- self.ui.on_email_validated(APP_NAME)
3694- self.assertEqual(((self.ui.window, APP_NAME), {}), self._called)
3695-
3696 def test_on_email_validation_error_verify_email_is_shown(self):
3697 """On email validation error, the verify_email page is shown."""
3698- self.ui.on_email_validation_error()
3699+ self.ui.on_email_validation_error(app_name=APP_NAME, error=ERROR)
3700 self.assert_pages_visibility(verify_email=True)
3701 self.assert_correct_warning(self.ui.warning_label,
3702 self.ui.UNKNOWN_ERROR)
3703
3704- def test_on_email_validation_error_proper_signals_is_emitted(self):
3705- """On email validation error, 'registration-failed' signal is sent."""
3706- self.ui.connect(gui.SIG_REGISTRATION_FAILED, self._set_called)
3707- self.ui.on_email_validation_error()
3708- self.assertEqual(((self.ui.window,), {}), self._called)
3709-
3710 def test_success_label_is_correct(self):
3711 """The success message is correct."""
3712 self.assertEqual(self.ui.SUCCESS, self.ui.success_label.get_text())
3713@@ -1063,7 +1035,7 @@
3714 expected = 'login'
3715 self.assertIn(expected, self.ui.backend._called)
3716 self.assertEqual(self.ui.backend._called[expected],
3717- ((EMAIL, PASSWORD, APP_NAME),
3718+ ((APP_NAME, EMAIL, PASSWORD),
3719 dict(reply_handler=gui.NO_OP,
3720 error_handler=gui.NO_OP)))
3721
3722@@ -1072,45 +1044,32 @@
3723 self.click_connect_with_valid_data()
3724 self.assert_pages_visibility(processing=True)
3725
3726- def test_on_logged_in_proper_signals_is_emitted(self):
3727- """On user logged in, 'login-succeeded' signal is sent."""
3728- self.ui.connect(gui.SIG_LOGIN_SUCCEEDED, self._set_called)
3729- self.ui.on_logged_in(APP_NAME)
3730- self.assertEqual(((self.ui.window, APP_NAME), {}), self._called)
3731-
3732 def test_on_logged_in_morphs_to_success_page(self):
3733 """When user logged in the success page is shown."""
3734 self.click_connect_with_valid_data()
3735- self.ui.on_logged_in(APP_NAME)
3736+ self.ui.on_logged_in(app_name=APP_NAME, email=EMAIL)
3737 self.assert_pages_visibility(success=True)
3738
3739 def test_on_login_error_morphs_to_login_page(self):
3740 """On user login error, the previous page is shown."""
3741 self.click_connect_with_valid_data()
3742- self.ui.on_login_error()
3743+ self.ui.on_login_error(app_name=APP_NAME, error=ERROR)
3744 self.assert_pages_visibility(login=True)
3745
3746 def test_on_login_error_a_warning_is_shown(self):
3747 """On user login error, a warning is shown with proper wording."""
3748 self.click_connect_with_valid_data()
3749- self.ui.on_login_error()
3750+ self.ui.on_login_error(app_name=APP_NAME, error=ERROR)
3751 self.assert_correct_warning(self.ui.warning_label,
3752 self.ui.UNKNOWN_ERROR)
3753
3754 def test_back_to_registration_hides_warning(self):
3755 """After user login error, warning is hidden when clicking 'Back'."""
3756 self.click_connect_with_valid_data()
3757- self.ui.on_login_error()
3758+ self.ui.on_login_error(app_name=APP_NAME, error=ERROR)
3759 self.ui.login_back_button.clicked()
3760 self.assertFalse(self.ui.warning_label.get_property('visible'))
3761
3762- def test_on_email_validation_error_proper_signals_is_emitted(self):
3763- """On user login error, 'login-failed' signal is sent."""
3764- self.ui.connect(gui.SIG_LOGIN_FAILED, self._set_called)
3765- self.click_connect_with_valid_data()
3766- self.ui.on_login_error()
3767- self.assertEqual(((self.ui.window,), {}), self._called)
3768-
3769
3770 class LoginValidationTestCase(UbuntuSSOClientTestCase):
3771 """Test suite for the user login validation."""
3772@@ -1231,20 +1190,20 @@
3773 expected = 'request_password_reset_token'
3774 self.assertIn(expected, self.ui.backend._called)
3775 self.assertEqual(self.ui.backend._called[expected],
3776- ((EMAIL,),
3777+ ((APP_NAME, EMAIL),
3778 dict(reply_handler=gui.NO_OP,
3779 error_handler=gui.NO_OP)))
3780
3781 def test_on_password_reset_token_sent_morphs_window(self):
3782 """When the reset token was sent, the reset password page is shown."""
3783 self.click_request_password_token_with_valid_data()
3784- self.ui.on_password_reset_token_sent(email=EMAIL)
3785+ self.ui.on_password_reset_token_sent(app_name=APP_NAME, email=EMAIL)
3786 self.assert_pages_visibility(set_new_password=True)
3787
3788 def test_on_password_reset_token_sent_help_text(self):
3789 """Clicking request_password_token_ok_button changes the help text."""
3790 self.click_request_password_token_with_valid_data()
3791- self.ui.on_password_reset_token_sent(email=EMAIL)
3792+ self.ui.on_password_reset_token_sent(app_name=APP_NAME, email=EMAIL)
3793
3794 self.assertEqual(self.ui.help_label.get_text(),
3795 self.ui.SET_NEW_PASSWORD_LABEL % {'email': EMAIL})
3796@@ -1252,14 +1211,14 @@
3797 def test_on_password_reset_token_sent_ok_button(self):
3798 """After request_password_token_ok_button the ok button is updated."""
3799 self.click_request_password_token_with_valid_data()
3800- self.ui.on_password_reset_token_sent(email=EMAIL)
3801+ self.ui.on_password_reset_token_sent(app_name=APP_NAME, email=EMAIL)
3802
3803 self.assertEqual(self.ui.set_new_password_ok_button.get_label(),
3804 self.ui.RESET_PASSWORD)
3805
3806 def test_on_password_reset_error_shows_login_page(self):
3807 """When reset token wasn't successfuly sent the login page is shown."""
3808- self.ui.on_password_reset_error()
3809+ self.ui.on_password_reset_error(app_name=APP_NAME, error=ERROR)
3810 self.assert_correct_warning(self.ui.warning_label,
3811 self.ui.UNKNOWN_ERROR)
3812 self.assert_pages_visibility(login=True)
3813@@ -1272,7 +1231,7 @@
3814 """Init."""
3815 super(SetNewPasswordTestCase, self).setUp()
3816 self.click_request_password_token_with_valid_data()
3817- self.ui.on_password_reset_token_sent(email=EMAIL)
3818+ self.ui.on_password_reset_token_sent(app_name=APP_NAME, email=EMAIL)
3819
3820 def test_on_set_new_password_ok_button_disabled(self):
3821 """The set_new_password_ok_button is disabled until values added."""
3822@@ -1303,20 +1262,20 @@
3823 expected = 'set_new_password'
3824 self.assertIn(expected, self.ui.backend._called)
3825 self.assertEqual(self.ui.backend._called[expected],
3826- ((EMAIL, RESET_PASSWORD_TOKEN, PASSWORD),
3827+ ((APP_NAME, EMAIL, RESET_PASSWORD_TOKEN, PASSWORD),
3828 dict(reply_handler=gui.NO_OP,
3829 error_handler=gui.NO_OP)))
3830
3831 def test_on_password_changed_shows_success_page(self):
3832 """When password was successfuly changed the success page is shown."""
3833- self.ui.on_password_changed(EMAIL)
3834+ self.ui.on_password_changed(app_name=APP_NAME, email=EMAIL)
3835 self.assert_correct_warning(self.ui.warning_label,
3836 self.ui.PASSWORD_CHANGED)
3837 self.assert_pages_visibility(login=True)
3838
3839 def test_on_password_change_error_shows_login_page(self):
3840 """When password wasn't changed the reset password page is shown."""
3841- self.ui.on_password_change_error()
3842+ self.ui.on_password_change_error(app_name=APP_NAME, error=ERROR)
3843 self.assert_correct_warning(self.ui.warning_label,
3844 self.ui.UNKNOWN_ERROR)
3845 self.assert_pages_visibility(request_password_token=True)
3846@@ -1365,3 +1324,107 @@
3847 def test_login_ok_button_has_the_focus(self):
3848 """The login_ok_button has the focus."""
3849 self.assertTrue(self.ui.login_ok_button.is_focus())
3850+
3851+
3852+class SignalsTestCase(UbuntuSSOClientTestCase):
3853+
3854+ def setUp(self):
3855+ """Init."""
3856+ super(SignalsTestCase, self).setUp()
3857+ self._called = {}
3858+ for sig_name, _ in gui.SIGNAL_ARGUMENTS:
3859+ self.ui.connect(sig_name, self._set_called, sig_name)
3860+
3861+ def _set_called(self, widget, *args, **kwargs):
3862+ """Keep trace of signals emition."""
3863+ self._called[args[-1]] = (widget, args[:-1], kwargs)
3864+
3865+ def test_every_cancel_emits_proper_signal(self):
3866+ """Clicking on any cancel button, 'user-cancelation' signal is sent."""
3867+ sig = gui.SIG_USER_CANCELATION
3868+ msg = 'user-cancelation is emitted when "%s" is clicked.'
3869+ buttons = filter(lambda name: 'cancel_button' in name, self.ui.widgets)
3870+ for name in buttons:
3871+ self.ui = self.gui_class(**self.kwargs)
3872+ self.ui.connect(sig, self._set_called, sig)
3873+ widget = getattr(self.ui, name)
3874+ widget.clicked()
3875+ expected_args = (self.ui.window, (APP_NAME,), {})
3876+ self.assertEqual(self._called[sig], expected_args, msg % name)
3877+ self._called = {}
3878+
3879+ def test_on_user_registration_error_proper_signal_is_emitted(self):
3880+ """On UserRegistrationError, 'registration-failed' signal is sent."""
3881+ self.ui.on_user_registration_error(app_name=APP_NAME, error=ERROR)
3882+ self.ui.on_close_clicked()
3883+ self.assertEqual((self.ui.window, (APP_NAME, ERROR), {}),
3884+ self._called[gui.SIG_REGISTRATION_FAILED])
3885+
3886+ def test_on_email_validated_proper_signals_is_emitted(self):
3887+ """On EmailValidated, 'registration-succeeded' signal is sent."""
3888+ self.ui.on_email_validated(app_name=APP_NAME, email=EMAIL)
3889+ self.ui.on_close_clicked()
3890+ self.assertEqual((self.ui.window, (APP_NAME, EMAIL), {}),
3891+ self._called[gui.SIG_REGISTRATION_SUCCEEDED])
3892+
3893+ def test_on_email_validation_error_proper_signals_is_emitted(self):
3894+ """On EmailValidationError, 'registration-failed' signal is sent."""
3895+ self.ui.on_email_validation_error(app_name=APP_NAME, error=ERROR)
3896+ self.ui.on_close_clicked()
3897+ self.assertEqual((self.ui.window, (APP_NAME, ERROR), {}),
3898+ self._called[gui.SIG_REGISTRATION_FAILED])
3899+
3900+ def test_on_logged_in_proper_signals_is_emitted(self):
3901+ """On LoggedIn, 'login-succeeded' signal is sent."""
3902+ self.ui.on_logged_in(app_name=APP_NAME, email=EMAIL)
3903+ self.ui.on_close_clicked()
3904+ self.assertEqual((self.ui.window, (APP_NAME, EMAIL), {}),
3905+ self._called[gui.SIG_LOGIN_SUCCEEDED])
3906+
3907+ def test_on_login_error_proper_signals_is_emitted(self):
3908+ """On LoginError, 'login-failed' signal is sent."""
3909+ self.click_connect_with_valid_data()
3910+ self.ui.on_login_error(app_name=APP_NAME, error=ERROR)
3911+ self.ui.on_close_clicked()
3912+ self.assertEqual((self.ui.window, (APP_NAME, ERROR), {}),
3913+ self._called[gui.SIG_LOGIN_FAILED])
3914+
3915+ def test_registration_successfull_even_if_prior_registration_error(self):
3916+ """Only one signal is sent with the final outcome."""
3917+ self.click_join_with_valid_data()
3918+ self.ui.on_user_registration_error(app_name=APP_NAME, error=ERROR)
3919+ self.click_join_with_valid_data()
3920+ self.ui.on_email_validated(app_name=APP_NAME, email=EMAIL)
3921+ self.ui.on_close_clicked()
3922+
3923+ self.assertEqual(len(self._called), 1)
3924+ self.assertTrue(gui.SIG_REGISTRATION_SUCCEEDED in self._called)
3925+
3926+ def test_login_successfull_even_if_prior_login_error(self):
3927+ """Only one signal is sent with the final outcome."""
3928+ self.click_connect_with_valid_data()
3929+ self.ui.on_login_error(app_name=APP_NAME, error=ERROR)
3930+ self.click_connect_with_valid_data()
3931+ self.ui.on_logged_in(app_name=APP_NAME, email=EMAIL)
3932+ self.ui.on_close_clicked()
3933+
3934+ self.assertEqual(len(self._called), 1)
3935+ self.assertTrue(gui.SIG_LOGIN_SUCCEEDED in self._called)
3936+
3937+ def test_user_cancelation_even_if_prior_registration_error(self):
3938+ """Only one signal is sent with the final outcome."""
3939+ self.click_join_with_valid_data()
3940+ self.ui.on_user_registration_error(app_name=APP_NAME, error=ERROR)
3941+ self.ui.join_cancel_button.clicked()
3942+
3943+ self.assertEqual(len(self._called), 1)
3944+ self.assertTrue(gui.SIG_USER_CANCELATION in self._called)
3945+
3946+ def test_user_cancelation_even_if_prior_login_error(self):
3947+ """Only one signal is sent with the final outcome."""
3948+ self.click_connect_with_valid_data()
3949+ self.ui.on_login_error(app_name=APP_NAME, error=ERROR)
3950+ self.ui.login_cancel_button.clicked()
3951+
3952+ self.assertEqual(len(self._called), 1)
3953+ self.assertTrue(gui.SIG_USER_CANCELATION in self._called)
3954
3955=== modified file 'ubuntu_sso/tests/test_main.py'
3956--- ubuntu_sso/tests/test_main.py 2010-08-12 00:00:08 +0000
3957+++ ubuntu_sso/tests/test_main.py 2010-08-24 20:27:39 +0000
3958@@ -22,11 +22,12 @@
3959 import logging
3960 import os
3961 import StringIO
3962+import urllib2
3963
3964 import gobject
3965
3966 from lazr.restfulclient.errors import HTTPError
3967-from mocker import Mocker, MockerTestCase, ARGS, KWARGS
3968+from mocker import Mocker, MockerTestCase, ARGS, KWARGS, ANY
3969 from twisted.internet.defer import Deferred
3970 from twisted.trial.unittest import TestCase
3971
3972@@ -36,10 +37,13 @@
3973 from ubuntu_sso import config, gui
3974 from ubuntu_sso.main import (
3975 AuthenticationError, BadRealmError, blocking, EmailTokenError,
3976- InvalidEmailError, InvalidPasswordError, SSOLogin, SSOCredentials,
3977- get_token_name, keyring_get_credentials, keyring_store_credentials, logger,
3978- LoginProcessor, NewPasswordError, SSOLoginProcessor, RegistrationError,
3979- ResetPasswordTokenError)
3980+ except_to_errdict, get_token_name,
3981+ InvalidEmailError, InvalidPasswordError,
3982+ keyring_get_credentials, keyring_store_credentials, logger,
3983+ LoginProcessor, NewPasswordError, OLD_KEY_NAME, PING_URL,
3984+ RegistrationError, ResetPasswordTokenError,
3985+ SSOCredentials, SSOLogin, SSOLoginProcessor,
3986+ U1_APP_NAME)
3987
3988
3989 APP_NAME = 'The Coolest App Ever'
3990@@ -60,15 +64,17 @@
3991 u'name': u'test',
3992 u'token': u'GkInOfSMGwTXAUoVQwLUoPxElEEUdhsLVNTPhxHJDUIeHCPNEo',
3993 u'token_secret': u'qFYImEtlczPbsCnYyuwLoPDlPEnvNcIktZphPQklAWrvyfFMV'}
3994+TOKEN_NAME = get_token_name(APP_NAME)
3995 STATUS_UNKNOWN = {'status': 'yadda-yadda'}
3996 STATUS_ERROR = {'status': 'error', 'errors': {'something': ['Bla', 'Ble']}}
3997 STATUS_OK = {'status': 'ok'}
3998 STATUS_EMAIL_UNKNOWN = {'status': 'yadda-yadda'}
3999 STATUS_EMAIL_ERROR = {'errors': {'email_token': ['Error1', 'Error2']}}
4000-STATUS_EMAIL_OK = {'email': 'okmail@okserver.okdomain'}
4001+STATUS_EMAIL_OK = {'email': EMAIL}
4002 TC_URL = 'tcurl'
4003 WINDOW_ID = 5
4004
4005+NO_OP = lambda *args, **kwargs: None
4006 LOGIN_OR_REGISTER_ARGS = (APP_NAME, TC_URL, HELP, WINDOW_ID)
4007 LOGIN_OR_REGISTER_GUI_ARGS = LOGIN_OR_REGISTER_ARGS + (False,)
4008 LOGIN_ONLY_ARGS = (APP_NAME, HELP, WINDOW_ID)
4009@@ -120,7 +126,7 @@
4010
4011 def authenticate(self, token_name):
4012 """Fake authenticate. Return a fix result."""
4013- if not token_name.startswith(get_token_name(APP_NAME)):
4014+ if not token_name.startswith(TOKEN_NAME):
4015 raise HTTPError(response=None, content=None)
4016 else:
4017 return TOKEN
4018@@ -159,13 +165,7 @@
4019 captcha_id=CAPTCHA_ID,
4020 captcha_solution=CAPTCHA_SOLUTION)
4021 self.login_kwargs = dict(email=EMAIL, password=PASSWORD,
4022- app_name=APP_NAME)
4023-
4024- def ksc(k, v):
4025- self.assertEqual(k, APP_NAME)
4026- self.assertEqual(v, TOKEN)
4027-
4028- self.patch(ubuntu_sso.main, "keyring_store_credentials", ksc)
4029+ token_name=TOKEN_NAME)
4030
4031 def tearDown(self):
4032 """Clean up."""
4033@@ -215,7 +215,7 @@
4034 def test_register_user_if_status_ok(self):
4035 """A user is succesfuy registered into the SSO server."""
4036 result = self.processor.register_user(**self.register_kwargs)
4037- self.assertTrue(result, 'registration was successful.')
4038+ self.assertEqual(EMAIL, result, 'registration was successful.')
4039
4040 def test_register_user_if_status_error(self):
4041 """Proper error is raised if register fails."""
4042@@ -238,7 +238,7 @@
4043
4044 def test_login_if_http_error(self):
4045 """Proper error is raised if authentication fails."""
4046- self.login_kwargs['app_name'] = APP_NAME * 2 # invalid token name
4047+ self.login_kwargs['token_name'] = APP_NAME * 2 # invalid token name
4048 self.assertRaises(AuthenticationError,
4049 self.processor.login, **self.login_kwargs)
4050
4051@@ -253,7 +253,7 @@
4052 """A email is succesfuy validated in the SSO server."""
4053 self.login_kwargs['email_token'] = EMAIL_TOKEN # valid email token
4054 result = self.processor.validate_email(**self.login_kwargs)
4055- self.assertTrue(result, 'email validation was successful.')
4056+ self.assertEqual(EMAIL, result, 'email validation was successful.')
4057
4058 def test_validate_email_if_status_error(self):
4059 """Proper error is raised if email validation fails."""
4060@@ -276,7 +276,8 @@
4061 def test_request_password_reset_token_if_status_ok(self):
4062 """A reset password token is succesfuly sent."""
4063 result = self.processor.request_password_reset_token(email=EMAIL)
4064- self.assertTrue(result, 'password reset token must be successful.')
4065+ self.assertEqual(EMAIL, result,
4066+ 'password reset token must be successful.')
4067
4068 def test_request_password_reset_token_if_http_error(self):
4069 """Proper error is raised if password token request fails."""
4070@@ -297,7 +298,8 @@
4071 result = self.processor.set_new_password(email=EMAIL,
4072 token=RESET_PASSWORD_TOKEN,
4073 new_password=PASSWORD)
4074- self.assertTrue(result, 'new password must be set successfully.')
4075+ self.assertEqual(EMAIL, result,
4076+ 'new password must be set successfully.')
4077
4078 def test_set_new_password_if_http_error(self):
4079 """Proper error is raised if setting a new password fails."""
4080@@ -399,19 +401,26 @@
4081 self.mocker.result(mockbus)
4082 mockbus._register_object_path(ARGS)
4083
4084+ def ksc(k, v):
4085+ """Assert over token and app_name."""
4086+ self.assertEqual(k, APP_NAME)
4087+ self.assertEqual(v, TOKEN)
4088+
4089+ self.patch(ubuntu_sso.main, "keyring_store_credentials", ksc)
4090+
4091 def tearDown(self):
4092 """Verify the mocking bus and shut it down."""
4093 self.mocker.verify()
4094 self.mocker.restore()
4095
4096- def fake_ok_blocking(self, f, cb, eb):
4097+ def fake_ok_blocking(self, f, a, cb, eb):
4098 """A fake blocking function that succeeds."""
4099- cb(f())
4100+ cb(a, f())
4101
4102- def fake_err_blocking(self, f, cb, eb):
4103+ def fake_err_blocking(self, f, a, cb, eb):
4104 """A fake blocking function that fails."""
4105 f()
4106- eb(BlockingSampleException())
4107+ eb(a, except_to_errdict(BlockingSampleException()))
4108
4109 def test_creation(self):
4110 """Test that the object creation is successful."""
4111@@ -436,15 +445,16 @@
4112 self.patch(ubuntu_sso.main, "blocking", self.fake_ok_blocking)
4113 self.mocker.replay()
4114
4115- def verify(result):
4116+ def verify(app_name, result):
4117 self.assertEqual(result, expected_result)
4118+ self.assertEqual(app_name, APP_NAME)
4119 d.callback(result)
4120
4121 client = SSOLogin(self.mockbusname,
4122 sso_login_processor_class=self.mockprocessorclass)
4123 self.patch(client, "CaptchaGenerated", verify)
4124 self.patch(client, "CaptchaGenerationError", d.errback)
4125- client.generate_captcha(filename)
4126+ client.generate_captcha(APP_NAME, filename)
4127 return d
4128
4129 def test_generate_captcha_error(self):
4130@@ -457,15 +467,16 @@
4131 self.patch(ubuntu_sso.main, "blocking", self.fake_err_blocking)
4132 self.mocker.replay()
4133
4134- def verify(error):
4135- self.assertIsInstance(error, BlockingSampleException)
4136+ def verify(app_name, errdict):
4137+ self.assertEqual(errdict["errtype"], "BlockingSampleException")
4138+ self.assertEqual(app_name, APP_NAME)
4139 d.callback("Ok")
4140
4141 client = SSOLogin(self.mockbusname,
4142 sso_login_processor_class=self.mockprocessorclass)
4143 self.patch(client, "CaptchaGenerated", d.errback)
4144 self.patch(client, "CaptchaGenerationError", verify)
4145- client.generate_captcha(filename)
4146+ client.generate_captcha(APP_NAME, filename)
4147 return d
4148
4149 def test_register_user(self):
4150@@ -478,15 +489,17 @@
4151 self.patch(ubuntu_sso.main, "blocking", self.fake_ok_blocking)
4152 self.mocker.replay()
4153
4154- def verify(result):
4155+ def verify(app_name, result):
4156 self.assertEqual(result, expected_result)
4157+ self.assertEqual(app_name, APP_NAME)
4158 d.callback(result)
4159
4160 client = SSOLogin(self.mockbusname,
4161 sso_login_processor_class=self.mockprocessorclass)
4162 self.patch(client, "UserRegistered", verify)
4163 self.patch(client, "UserRegistrationError", d.errback)
4164- client.register_user(EMAIL, PASSWORD, CAPTCHA_ID, CAPTCHA_SOLUTION)
4165+ client.register_user(APP_NAME, EMAIL, PASSWORD, CAPTCHA_ID,
4166+ CAPTCHA_SOLUTION)
4167 return d
4168
4169 def test_register_user_error(self):
4170@@ -499,75 +512,78 @@
4171 self.patch(ubuntu_sso.main, "blocking", self.fake_err_blocking)
4172 self.mocker.replay()
4173
4174- def verify(error):
4175- self.assertIsInstance(error, BlockingSampleException)
4176+ def verify(app_name, errdict):
4177+ self.assertEqual(errdict["errtype"], "BlockingSampleException")
4178+ self.assertEqual(app_name, APP_NAME)
4179 d.callback("Ok")
4180
4181 client = SSOLogin(self.mockbusname,
4182 sso_login_processor_class=self.mockprocessorclass)
4183 self.patch(client, "UserRegistered", d.errback)
4184 self.patch(client, "UserRegistrationError", verify)
4185- client.register_user(EMAIL, PASSWORD, CAPTCHA_ID, CAPTCHA_SOLUTION)
4186+ client.register_user(APP_NAME, EMAIL, PASSWORD, CAPTCHA_ID,
4187+ CAPTCHA_SOLUTION)
4188 return d
4189
4190 def test_login(self):
4191 """Test that the login method works ok."""
4192 d = Deferred()
4193- sample_result = {"sample": "tokens"}
4194- self.create_mock_processor().login(EMAIL, PASSWORD, APP_NAME)
4195- self.mocker.result(sample_result)
4196+ self.create_mock_processor().login(EMAIL, PASSWORD, TOKEN_NAME)
4197+ self.mocker.result(TOKEN)
4198 self.patch(ubuntu_sso.main, "blocking", self.fake_ok_blocking)
4199 self.mocker.replay()
4200
4201- def verify(result):
4202- self.assertEqual(result, APP_NAME)
4203+ def verify(app_name, result):
4204+ self.assertEqual(result, EMAIL)
4205+ self.assertEqual(app_name, APP_NAME)
4206 d.callback(result)
4207
4208 client = SSOLogin(self.mockbusname,
4209 sso_login_processor_class=self.mockprocessorclass)
4210 self.patch(client, "LoggedIn", verify)
4211 self.patch(client, "LoginError", d.errback)
4212- client.login(EMAIL, PASSWORD, APP_NAME)
4213+ client.login(APP_NAME, EMAIL, PASSWORD)
4214 return d
4215
4216 def test_login_error(self):
4217 """Test that the login method fails as expected."""
4218 d = Deferred()
4219- expected_result = "expected result"
4220- self.create_mock_processor().login(EMAIL, PASSWORD, APP_NAME)
4221- self.mocker.result(expected_result)
4222+ self.create_mock_processor().login(EMAIL, PASSWORD, TOKEN_NAME)
4223+ self.mocker.result(TOKEN)
4224 self.patch(ubuntu_sso.main, "blocking", self.fake_err_blocking)
4225 self.mocker.replay()
4226
4227- def verify(error):
4228- self.assertIsInstance(error, BlockingSampleException)
4229+ def verify(app_name, errdict):
4230+ self.assertEqual(app_name, APP_NAME)
4231+ self.assertEqual(errdict["errtype"], "BlockingSampleException")
4232 d.callback("Ok")
4233
4234 client = SSOLogin(self.mockbusname,
4235 sso_login_processor_class=self.mockprocessorclass)
4236 self.patch(client, "LoggedIn", d.errback)
4237 self.patch(client, "LoginError", verify)
4238- client.login(EMAIL, PASSWORD, APP_NAME)
4239+ client.login(APP_NAME, EMAIL, PASSWORD)
4240 return d
4241
4242 def test_validate_email(self):
4243 """Test that the validate_email method works ok."""
4244 d = Deferred()
4245 self.create_mock_processor().validate_email(EMAIL, PASSWORD,
4246- APP_NAME, EMAIL_TOKEN)
4247- self.mocker.result(APP_NAME)
4248+ EMAIL_TOKEN, TOKEN_NAME)
4249+ self.mocker.result(EMAIL)
4250 self.patch(ubuntu_sso.main, "blocking", self.fake_ok_blocking)
4251 self.mocker.replay()
4252
4253- def verify(result):
4254- self.assertEqual(result, APP_NAME)
4255+ def verify(app_name, result):
4256+ self.assertEqual(result, EMAIL)
4257+ self.assertEqual(app_name, APP_NAME)
4258 d.callback(result)
4259
4260 client = SSOLogin(self.mockbusname,
4261 sso_login_processor_class=self.mockprocessorclass)
4262 self.patch(client, "EmailValidated", verify)
4263 self.patch(client, "EmailValidationError", d.errback)
4264- client.validate_email(EMAIL, PASSWORD, APP_NAME, EMAIL_TOKEN)
4265+ client.validate_email(APP_NAME, EMAIL, PASSWORD, EMAIL_TOKEN)
4266 return d
4267
4268 def test_validate_email_error(self):
4269@@ -576,20 +592,21 @@
4270 expected_result = "expected result"
4271
4272 self.create_mock_processor().validate_email(EMAIL, PASSWORD,
4273- APP_NAME, EMAIL_TOKEN)
4274+ EMAIL_TOKEN, TOKEN_NAME)
4275 self.mocker.result(expected_result)
4276 self.patch(ubuntu_sso.main, "blocking", self.fake_err_blocking)
4277 self.mocker.replay()
4278
4279- def verify(error):
4280- self.assertIsInstance(error, BlockingSampleException)
4281+ def verify(app_name, errdict):
4282+ self.assertEqual(app_name, APP_NAME)
4283+ self.assertEqual(errdict["errtype"], "BlockingSampleException")
4284 d.callback("Ok")
4285
4286 client = SSOLogin(self.mockbusname,
4287 sso_login_processor_class=self.mockprocessorclass)
4288 self.patch(client, "EmailValidated", d.errback)
4289 self.patch(client, "EmailValidationError", verify)
4290- client.validate_email(EMAIL, PASSWORD, APP_NAME, EMAIL_TOKEN)
4291+ client.validate_email(APP_NAME, EMAIL, PASSWORD, EMAIL_TOKEN)
4292 return d
4293
4294 def test_request_password_reset_token(self):
4295@@ -598,17 +615,19 @@
4296 processor = self.create_mock_processor()
4297 processor.request_password_reset_token(EMAIL)
4298 self.patch(ubuntu_sso.main, "blocking", self.fake_ok_blocking)
4299+ self.mocker.result(EMAIL)
4300 self.mocker.replay()
4301
4302- def verify(result):
4303+ def verify(app_name, result):
4304 self.assertEqual(result, EMAIL)
4305+ self.assertEqual(app_name, APP_NAME)
4306 d.callback(result)
4307
4308 client = SSOLogin(self.mockbusname,
4309 sso_login_processor_class=self.mockprocessorclass)
4310 self.patch(client, "PasswordResetTokenSent", verify)
4311 self.patch(client, "PasswordResetError", d.errback)
4312- client.request_password_reset_token(EMAIL)
4313+ client.request_password_reset_token(APP_NAME, EMAIL)
4314 return d
4315
4316 def test_request_password_reset_token_error(self):
4317@@ -616,19 +635,20 @@
4318 d = Deferred()
4319
4320 self.create_mock_processor().request_password_reset_token(EMAIL)
4321- self.mocker.result(True)
4322+ self.mocker.result(EMAIL)
4323 self.patch(ubuntu_sso.main, "blocking", self.fake_err_blocking)
4324 self.mocker.replay()
4325
4326- def verify(error):
4327- self.assertIsInstance(error, BlockingSampleException)
4328+ def verify(app_name, errdict):
4329+ self.assertEqual(errdict["errtype"], "BlockingSampleException")
4330+ self.assertEqual(app_name, APP_NAME)
4331 d.callback("Ok")
4332
4333 client = SSOLogin(self.mockbusname,
4334 sso_login_processor_class=self.mockprocessorclass)
4335 self.patch(client, "PasswordResetTokenSent", d.errback)
4336 self.patch(client, "PasswordResetError", verify)
4337- client.request_password_reset_token(EMAIL)
4338+ client.request_password_reset_token(APP_NAME, EMAIL)
4339 return d
4340
4341 def test_set_new_password(self):
4342@@ -640,15 +660,16 @@
4343 self.patch(ubuntu_sso.main, "blocking", self.fake_ok_blocking)
4344 self.mocker.replay()
4345
4346- def verify(result):
4347+ def verify(app_name, result):
4348 self.assertEqual(result, EMAIL)
4349+ self.assertEqual(app_name, APP_NAME)
4350 d.callback(result)
4351
4352 client = SSOLogin(self.mockbusname,
4353 sso_login_processor_class=self.mockprocessorclass)
4354 self.patch(client, "PasswordChanged", verify)
4355 self.patch(client, "PasswordChangeError", d.errback)
4356- client.set_new_password(EMAIL, EMAIL_TOKEN, PASSWORD)
4357+ client.set_new_password(APP_NAME, EMAIL, EMAIL_TOKEN, PASSWORD)
4358 return d
4359
4360 def test_set_new_password_error(self):
4361@@ -662,21 +683,24 @@
4362 self.patch(ubuntu_sso.main, "blocking", self.fake_err_blocking)
4363 self.mocker.replay()
4364
4365- def verify(error):
4366- self.assertIsInstance(error, BlockingSampleException)
4367+ def verify(app_name, errdict):
4368+ self.assertEqual(errdict["errtype"], "BlockingSampleException")
4369+ self.assertEqual(app_name, APP_NAME)
4370 d.callback("Ok")
4371
4372 client = SSOLogin(self.mockbusname,
4373 sso_login_processor_class=self.mockprocessorclass)
4374 self.patch(client, "PasswordChanged", d.errback)
4375 self.patch(client, "PasswordChangeError", verify)
4376- client.set_new_password(EMAIL, EMAIL_TOKEN, PASSWORD)
4377+ client.set_new_password(APP_NAME, EMAIL, EMAIL_TOKEN, PASSWORD)
4378 return d
4379
4380
4381 class BlockingFunctionTestCase(TestCase):
4382 """Tests for the "blocking" function."""
4383
4384+ timeout = 5
4385+
4386 def test_blocking(self):
4387 """Test the normal behaviour."""
4388 d = Deferred()
4389@@ -685,11 +709,12 @@
4390 def f():
4391 return expected_result
4392
4393- def verify(result):
4394+ def verify(app_name, result):
4395 self.assertEqual(result, expected_result)
4396+ self.assertEqual(app_name, APP_NAME)
4397 d.callback(result)
4398
4399- blocking(f, verify, d.errback)
4400+ blocking(f, APP_NAME, verify, d.errback)
4401 return d
4402
4403 def test_blocking_error(self):
4404@@ -700,11 +725,13 @@
4405 def f():
4406 raise BlockingSampleException(expected_error_message)
4407
4408- def verify(error):
4409- self.assertEqual(error, expected_error_message)
4410+ def verify(app_name, errdict):
4411+ self.assertEqual(app_name, APP_NAME)
4412+ self.assertEqual(errdict["errtype"], "BlockingSampleException")
4413+ self.assertEqual(errdict["message"], expected_error_message)
4414 d.callback("Ok")
4415
4416- blocking(f, d.errback, verify)
4417+ blocking(f, APP_NAME, d.errback, verify)
4418 return d
4419
4420
4421@@ -749,6 +776,58 @@
4422 token = keyring_get_credentials(APP_NAME)
4423 self.assertEqual(token, None)
4424
4425+ def test_keyring_get_old_cred_found(self):
4426+ """The method returns a new set of creds if old creds are found."""
4427+ sample_oauth_token = "sample oauth token"
4428+ sample_oauth_secret = "sample oauth secret"
4429+ old_creds = {
4430+ "oauth_token": sample_oauth_token,
4431+ "oauth_token_secret": sample_oauth_secret,
4432+ }
4433+
4434+ mockKeyringClass = self.mocker.replace("ubuntu_sso.keyring.Keyring")
4435+ mockKeyringClass(U1_APP_NAME)
4436+ mockKeyring = self.mocker.mock()
4437+ self.mocker.result(mockKeyring)
4438+ mockKeyring.get_ubuntusso_attr()
4439+ self.mocker.result(None)
4440+
4441+ mockKeyringClass(OLD_KEY_NAME)
4442+ mockKeyring2 = self.mocker.mock()
4443+ self.mocker.result(mockKeyring2)
4444+ mockKeyring2.get_ubuntusso_attr()
4445+ self.mocker.result(old_creds)
4446+
4447+ self.mocker.replay()
4448+
4449+ new_creds = keyring_get_credentials(U1_APP_NAME)
4450+ self.assertIn("token", new_creds)
4451+ self.assertEqual(new_creds["token"], sample_oauth_token)
4452+ self.assertIn("token_secret", new_creds)
4453+ self.assertEqual(new_creds["token_secret"], sample_oauth_secret)
4454+ self.assertIn("token_name", new_creds)
4455+ self.assertEqual(new_creds["token_name"], OLD_KEY_NAME)
4456+
4457+ def test_keyring_get_old_cred_not_found(self):
4458+ """The method returns None if no old nor new credentials found."""
4459+ mockKeyringClass = self.mocker.replace("ubuntu_sso.keyring.Keyring")
4460+ mockKeyringClass(U1_APP_NAME)
4461+ mockKeyring = self.mocker.mock()
4462+ self.mocker.result(mockKeyring)
4463+ mockKeyring.get_ubuntusso_attr()
4464+ self.mocker.result(None)
4465+
4466+ mockKeyringClass(OLD_KEY_NAME)
4467+ mockKeyring2 = self.mocker.mock()
4468+ self.mocker.result(mockKeyring2)
4469+ mockKeyring2.get_ubuntusso_attr()
4470+ self.mocker.result(None)
4471+
4472+ self.mocker.replay()
4473+
4474+ token = keyring_get_credentials(U1_APP_NAME)
4475+ self.assertEqual(token, None)
4476+
4477
4478 class RegisterSampleException(Exception):
4479 """A mock exception thrown just when testing."""
4480@@ -788,8 +867,8 @@
4481 expected_creds = TOKEN
4482 d = Deferred()
4483
4484- def verify(result):
4485- self.assertEqual(result, expected_creds)
4486+ def verify(app_name, credentials):
4487+ self.assertEqual(credentials, expected_creds)
4488 d.callback("ok")
4489
4490 kgt = self.mocker.replace("ubuntu_sso.main.keyring_get_credentials")
4491@@ -828,8 +907,8 @@
4492 expected_error = "Sample Error - not for resale"
4493 d = Deferred()
4494
4495- def verify(result, *a):
4496- self.assertEqual(result, expected_error)
4497+ def verify(app_name, error_message, detailed_error):
4498+ self.assertEqual(app_name, APP_NAME)
4499 d.callback("ok")
4500
4501 kgt = self.mocker.replace("ubuntu_sso.main.keyring_get_credentials")
4502@@ -849,8 +928,8 @@
4503 expected_creds = TOKEN
4504 d = Deferred()
4505
4506- def verify(result):
4507- self.assertEqual(result, expected_creds)
4508+ def verify(app_name, credentials):
4509+ self.assertEqual(credentials, expected_creds)
4510 d.callback("ok")
4511
4512 kgt = self.mocker.replace("ubuntu_sso.main.keyring_get_credentials")
4513@@ -889,8 +968,8 @@
4514 expected_error = "Sample Error - not for resale"
4515 d = Deferred()
4516
4517- def verify(result, *a):
4518- self.assertEqual(result, expected_error)
4519+ def verify(app_name, error_message, detailed_error):
4520+ self.assertEqual(app_name, APP_NAME)
4521 d.callback("ok")
4522
4523 kgt = self.mocker.replace("ubuntu_sso.main.keyring_get_credentials")
4524@@ -905,6 +984,115 @@
4525 client.login_to_get_credentials(*LOGIN_ONLY_ARGS)
4526 return d
4527
4528+ def test_clear_token(self):
4529+ """Check that clear_token tries removing the correct token."""
4530+ mockKeyringClass = self.mocker.replace("ubuntu_sso.keyring.Keyring")
4531+ mockKeyringClass(APP_NAME)
4532+ mockKeyring = self.mocker.mock()
4533+ self.mocker.result(mockKeyring)
4534+ mockKeyring.delete_ubuntusso_attr()
4535+ self.mocker.replay()
4536+
4537+ client = SSOCredentials(self.mocker.mock())
4538+ client.clear_token(APP_NAME)
4539+
4540+ def test_clear_token_failed(self):
4541+ """Check that clear_token fails correctly."""
4542+ mockKeyringClass = self.mocker.replace("ubuntu_sso.keyring.Keyring")
4543+ mockKeyringClass(APP_NAME)
4544+ self.mocker.throw(self.mocker.mock())
4545+ fake_logger = self.mocker.replace("ubuntu_sso.main.logger")
4546+ fake_logger.exception(ANY, APP_NAME)
4547+ self.mocker.replay()
4548+
4549+ client = SSOCredentials(self.mocker.mock())
4550+ client.clear_token(APP_NAME)
4551+
4552+ def test_login_error_cb(self):
4553+ """The login error callback should throw the signal."""
4554+ d = Deferred()
4555+
4556+ def verify(app_name, error_message, detailed_error):
4557+ self.assertEqual(app_name, APP_NAME)
4558+ d.callback("ok")
4559+
4560+ self.mocker.replay()
4561+ client = SSOCredentials(self.mocker.mock())
4562+ self.patch(client, "CredentialsError", verify)
4563+
4564+ client._login_error_cb(None, APP_NAME, 'some error')
4565+ return d
4566+
4567+ def test_show_login_or_register_ui_error(self):
4568+ """An error happens when trying to register."""
4569+ d = Deferred()
4570+ mockgui = self.mocker.replace("ubuntu_sso.gui.UbuntuSSOClientGUI")
4571+ mockgui(ARGS)
4572+ self.mocker.throw(Exception())
4573+ self.mocker.replay()
4574+
4575+ def verify(app_name, msg, full_error):
4576+ self.assertEqual(app_name, APP_NAME)
4577+ d.callback("ok")
4578+
4579+ client = SSOCredentials(self.mocker.mock())
4580+ self.patch(client, "CredentialsError", verify)
4581+
4582+ client._show_login_or_register_ui(APP_NAME, "http:tc_url", "help", 0)
4583+ return d
4584+
4585+ def test_login_success_cb_works(self):
4586+ """Check that the right signal is sent."""
4587+ expected_creds = TOKEN
4588+ d = Deferred()
4589+ kgc = self.mocker.replace("ubuntu_sso.main.keyring_get_credentials")
4590+ kgc(ARGS)
4591+ self.mocker.result(expected_creds)
4592+ self.mocker.replay()
4593+
4594+ def verify(app_name, credentials):
4595+ self.assertEqual(credentials, expected_creds)
4596+ d.callback("ok")
4597+
4598+ client = SSOCredentials(self.mocker.mock())
4599+ self.patch(client, "CredentialsFound", verify)
4600+
4601+ client._login_success_cb(None, APP_NAME, EMAIL)
4602+ return d
4603+
4604+ def test_login_success_cb_error(self):
4605+ """An error happens when accessing the keyring."""
4606+ d = Deferred()
4607+ kgc = self.mocker.replace("ubuntu_sso.main.keyring_get_credentials")
4608+ kgc(ARGS)
4609+ self.mocker.throw(Exception())
4610+ self.mocker.replay()
4611+
4612+ def verify(app_name, msg, full_error):
4613+ self.assertEqual(app_name, APP_NAME)
4614+ d.callback("ok")
4615+
4616+ client = SSOCredentials(self.mocker.mock())
4617+ self.patch(client, "CredentialsError", verify)
4618+
4619+ client._login_success_cb(None, APP_NAME, EMAIL)
4620+ return d
4621+
4622+ def test_auth_denied_cb(self):
4623+ """When the user decides not to allow the registration or login."""
4624+ d = Deferred()
4625+ self.mocker.replay()
4626+
4627+ def verify(app_name):
4628+ self.assertEqual(app_name, APP_NAME)
4629+ d.callback("ok")
4630+
4631+ client = SSOCredentials(self.mocker.mock())
4632+ self.patch(client, "AuthorizationDenied", verify)
4633+
4634+ client._login_auth_denied_cb(None, APP_NAME)
4635+ return d
4636+
4637
4638 class CredentialsGUITestCase(TestCase):
4639 """login_or_register_to_get_credentials opens the proper GUI."""
4640@@ -975,8 +1163,10 @@
4641
4642 expected = [
4643 (gui.SIG_LOGIN_SUCCEEDED, self.client._login_success_cb),
4644+ (gui.SIG_LOGIN_SUCCEEDED, self.client._ping_url),
4645 (gui.SIG_LOGIN_FAILED, self.client._login_error_cb),
4646 (gui.SIG_REGISTRATION_SUCCEEDED, self.client._login_success_cb),
4647+ (gui.SIG_REGISTRATION_SUCCEEDED, self.client._ping_url),
4648 (gui.SIG_REGISTRATION_FAILED, self.client._login_error_cb),
4649 (gui.SIG_USER_CANCELATION, self.client._login_auth_denied_cb),
4650 ]
4651@@ -1035,11 +1225,47 @@
4652
4653 expected = [
4654 (gui.SIG_LOGIN_SUCCEEDED, self.client._login_success_cb),
4655+ (gui.SIG_LOGIN_SUCCEEDED, self.client._ping_url),
4656 (gui.SIG_LOGIN_FAILED, self.client._login_error_cb),
4657 (gui.SIG_REGISTRATION_SUCCEEDED, self.client._login_success_cb),
4658+ (gui.SIG_REGISTRATION_SUCCEEDED, self.client._ping_url),
4659 (gui.SIG_REGISTRATION_FAILED, self.client._login_error_cb),
4660 (gui.SIG_USER_CANCELATION, self.client._login_auth_denied_cb),
4661 ]
4662 self.patch(gui, "UbuntuSSOClientGUI", FakedUbuntuSSOClientGUI)
4663 self.login_only(*LOGIN_ONLY_ARGS)
4664 self.assertEqual(self.signals, expected)
4665+
4666+ def test_on_registration_successful_u1_server_is_pinged(self):
4667+ """When a registration is successful, the PING_URL is pinged."""
4668+ self.args = None
4669+ self.kwargs = None
4670+
4671+ def fake_it(*args, **kwargs):
4672+ """Fake a call."""
4673+ self.args = args
4674+ self.kwargs = kwargs
4675+
4676+ self.patch(urllib2, 'urlopen', fake_it)
4677+
4678+ self.client._ping_url(None, U1_APP_NAME, EMAIL)
4679+
4680+ self.assertEqual(self.args, (PING_URL + EMAIL,))
4681+ self.assertEqual(self.kwargs, {})
4682+
4683+ def test_on_registration_successful_no_server_is_pinged(self):
4684+ """When a registration is successful, the PING_URL is not pinged."""
4685+ self.args = None
4686+ self.kwargs = None
4687+
4688+ def fake_it(*args, **kwargs):
4689+ """Fake a call."""
4690+ self.args = args
4691+ self.kwargs = kwargs
4692+
4693+ self.patch(urllib2, 'urlopen', fake_it)
4694+
4695+ self.client._ping_url(None, APP_NAME, EMAIL)
4696+
4697+ self.assertEqual(self.args, None)
4698+ self.assertEqual(self.kwargs, None)

Subscribers

People subscribed via source and target branches

to all changes: