Merge lp:~dobey/software-center/update-13-10 into lp:software-center/stable-13-10

Proposed by dobey
Status: Merged
Approved by: dobey
Approved revision: 3313
Merged at revision: 3312
Proposed branch: lp:~dobey/software-center/update-13-10
Merge into: lp:software-center/stable-13-10
Diff against target: 4850 lines (+615/-3764)
25 files modified
bin/software-center-sso-gtk (+0/-36)
setup.py (+0/-4)
softwarecenter/backend/installbackend_impl/aptd.py (+5/-0)
softwarecenter/backend/login_impl/login_sso.py (+2/-3)
softwarecenter/backend/unitylauncher.py (+51/-24)
softwarecenter/backend/zeitgeist_logger.py (+97/-0)
softwarecenter/db/categories.py (+3/-1)
softwarecenter/db/database.py (+1/-1)
softwarecenter/distro/__init__.py (+6/-0)
softwarecenter/distro/fedora.py (+3/-0)
softwarecenter/distro/ubuntu.py (+3/-0)
softwarecenter/sso/__init__.py (+0/-18)
softwarecenter/sso/gui.py (+0/-1168)
softwarecenter/sso/tests/__init__.py (+0/-27)
softwarecenter/sso/tests/test_gui.py (+0/-2300)
softwarecenter/ui/gtk3/panes/availablepane.py (+103/-79)
softwarecenter/ui/gtk3/widgets/stars.py (+1/-1)
softwarecenter/utils.py (+20/-8)
tests/gtk3/test_navhistory.py (+1/-1)
tests/gtk3/test_unity_launcher_integration_gui.py (+30/-91)
tests/gtk3/test_zeitgeist_logger_gui.py (+95/-0)
tests/gtk3/windows.py (+8/-2)
tests/test_unity_launcher.py (+48/-0)
tests/test_utils.py (+52/-0)
tests/test_zeitgeist_logger.py (+86/-0)
To merge this branch: bzr merge lp:~dobey/software-center/update-13-10
Reviewer Review Type Date Requested Status
Alejandro J. Cura (community) Approve
Roberto Alsina Approve
Review via email: mp+186073@code.launchpad.net

Commit message

[Rodney Dawes]

    Fix some RuntimeWarnings about old-style classes.
    Remove the old gtk+ based SSO UI, and rely on the system library and UI.
    Use the "Ubuntu One" token for authenticating to the server now.

[Marco Trevisan]

    Log zeitgeist events on application install/removal.

[Michael Vogt]

    Improve Unity launcher integration.

[Sv. Lockal]

    Fix UnicodeDecodeError for localized category names.

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

+1

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== removed file 'bin/software-center-sso-gtk'
2--- bin/software-center-sso-gtk 2013-04-17 17:36:54 +0000
3+++ bin/software-center-sso-gtk 1970-01-01 00:00:00 +0000
4@@ -1,36 +0,0 @@
5-#!/usr/bin/env python
6-# -*- coding: utf-8 -*-
7-#
8-# Copyright 2012 Canonical Ltd.
9-#
10-# This program is free software: you can redistribute it and/or modify it
11-# under the terms of the GNU General Public License version 3, as published
12-# by the Free Software Foundation.
13-#
14-# This program is distributed in the hope that it will be useful, but
15-# WITHOUT ANY WARRANTY; without even the implied warranties of
16-# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
17-# PURPOSE. See the GNU General Public License for more details.
18-#
19-# You should have received a copy of the GNU General Public License along
20-# with this program. If not, see <http://www.gnu.org/licenses/>.
21-#
22-
23-"""Start the SSO GTK+ UI."""
24-
25-# Invalid name "software-center-sso-gtk", pylint: disable=C0103
26-# Access to a protected member, pylint: disable=W0212
27-
28-import sys
29-sys.path.insert(0, "/usr/share/software-center")
30-
31-from softwarecenter.sso import gui
32-from ubuntu_sso.utils.ui import parse_args
33-
34-from dbus.mainloop.glib import DBusGMainLoop
35-DBusGMainLoop(set_as_default=True)
36-
37-
38-if __name__ == "__main__":
39- args = parse_args()
40- gui.run(**dict(args._get_kwargs()))
41
42=== modified file 'setup.py'
43--- setup.py 2013-08-06 19:23:26 +0000
44+++ setup.py 2013-09-17 17:52:06 +0000
45@@ -95,7 +95,6 @@
46 'softwarecenter.db.history_impl',
47 'softwarecenter.distro',
48 'softwarecenter.plugins',
49- 'softwarecenter.sso',
50 'softwarecenter.ui',
51 'softwarecenter.ui.gtk3',
52 'softwarecenter.ui.gtk3.dialogs',
53@@ -117,8 +116,6 @@
54 glob.glob("data/ui/gtk3/art/icons/*.png")),
55 ('share/software-center/default_banner',
56 glob.glob("data/default_banner/*")),
57- # sso
58- ('share/software-center/ui/sso/', glob.glob("data/ui/sso/*.ui")),
59 # dbus
60 ('../etc/dbus-1/system.d/',
61 ["data/dbus/com.ubuntu.SoftwareCenter.conf"]),
62@@ -137,6 +134,5 @@
63 # extra software channels (can be distro specific)
64 ('share/app-install/channels/',
65 glob.glob("data/channels/%s/*.{eula,list}" % distro)),
66- ('lib/ubuntu-sso-client', ['bin/software-center-sso-gtk']),
67 ],
68 )
69
70=== modified file 'softwarecenter/backend/installbackend_impl/aptd.py'
71--- softwarecenter/backend/installbackend_impl/aptd.py 2012-12-14 16:44:25 +0000
72+++ softwarecenter/backend/installbackend_impl/aptd.py 2013-09-17 17:52:06 +0000
73@@ -716,6 +716,11 @@
74 trans = yield self.aptd_client.install_packages(
75 [app.pkgname], defer=True)
76 self._logger.info("run_transaction()")
77+ # notify about the install so that the unity-launcher
78+ # integration works
79+ self.emit("transaction-started",
80+ app.pkgname, app.appname, trans.tid,
81+ TransactionTypes.INSTALL)
82 yield self._run_transaction(trans, app.pkgname, app.appname,
83 "", metadata)
84 except Exception as error:
85
86=== modified file 'softwarecenter/backend/login_impl/login_sso.py'
87--- softwarecenter/backend/login_impl/login_sso.py 2012-11-28 17:36:38 +0000
88+++ softwarecenter/backend/login_impl/login_sso.py 2013-09-17 17:52:06 +0000
89@@ -41,7 +41,7 @@
90
91 def __init__(self, window_id, appname, help_text):
92 super(LoginBackendDbusSSO, self).__init__()
93- self.appname = appname
94+ self.appname = 'Ubuntu One'
95 self.help_text = help_text
96 self.bus = dbus.SessionBus()
97 obj = self.bus.get_object(bus_name=DBUS_BUS_NAME,
98@@ -61,8 +61,7 @@
99 self._credentials = None
100
101 def _get_params(self):
102- p = {'ui_executable': 'software-center-sso-gtk',
103- }
104+ p = {}
105 if self.help_text:
106 p['help_text'] = utf8(self.help_text)
107 if self._window_id:
108
109=== modified file 'softwarecenter/backend/unitylauncher.py'
110--- softwarecenter/backend/unitylauncher.py 2012-11-23 22:57:21 +0000
111+++ softwarecenter/backend/unitylauncher.py 2013-09-17 17:52:06 +0000
112@@ -2,6 +2,7 @@
113 #
114 # Authors:
115 # Gary Lasker
116+# Michael Vogt
117 #
118 # This program is free software; you can redistribute it and/or modify it under
119 # the terms of the GNU General Public License as published by the Free Software
120@@ -18,6 +19,8 @@
121
122 import dbus
123 import logging
124+import os
125+import tempfile
126
127 LOG = logging.getLogger(__name__)
128
129@@ -45,44 +48,68 @@
130 self.trans_id = trans_id
131
132
133-class TransactionDetails(object):
134- """ Simple class to keep track of aptdaemon transaction details for
135- use with the Unity launcher integration
136- """
137- def __init__(self,
138- pkgname,
139- appname,
140- trans_id,
141- trans_type):
142- self.pkgname = pkgname
143- self.appname = appname
144- self.trans_id = trans_id
145- self.trans_type = trans_type
146-
147-
148 class UnityLauncher(object):
149 """ Implements the integration between Software Center and the Unity
150 launcher
151 """
152
153+ def __init__(self):
154+ self._pkgname_to_temp_desktop_file = {}
155+
156+ def _get_launcher_dbus_iface(self):
157+ bus = dbus.SessionBus()
158+ launcher_obj = bus.get_object('com.canonical.Unity.Launcher',
159+ '/com/canonical/Unity/Launcher')
160+ launcher_iface = dbus.Interface(launcher_obj,
161+ 'com.canonical.Unity.Launcher')
162+ return launcher_iface
163+
164+ def cancel_application_to_launcher(self, pkgname):
165+ filename = self._pkgname_to_temp_desktop_file.pop(pkgname, None)
166+ if filename:
167+ os.unlink(filename)
168+
169+ def _get_temp_desktop_file(self, pkgname, launcher_info):
170+ with tempfile.NamedTemporaryFile(prefix="software-center-agent:",
171+ suffix=":%s.desktop" % pkgname,
172+ delete=False) as fp:
173+ s = """
174+[Desktop Entry]
175+Name=%(name)s
176+Icon=%(icon_file_path)s
177+Type=Application""" % {'name': launcher_info.name,
178+ 'icon_file_path': launcher_info.icon_file_path,
179+ }
180+ fp.write(s)
181+ fp.flush()
182+ LOG.debug("create temp desktop file '%s'" % fp.name)
183+ return fp.name
184+
185 def send_application_to_launcher(self, pkgname, launcher_info):
186 """ send a dbus message to the Unity launcher service to initiate
187 the add-to-launcher functionality for the specified application
188 """
189- LOG.debug("sending dbus signal to Unity launcher for application: ",
190+
191+ # stuff from the agent has no desktop file so we create a fake
192+ # one here just for the install
193+ if (launcher_info.installed_desktop_file_path ==
194+ "software-center-agent"):
195+ temp_desktop = self._get_temp_desktop_file(pkgname, launcher_info)
196+ launcher_info.installed_desktop_file_path = temp_desktop
197+ self._pkgname_to_temp_desktop_file[pkgname] = temp_desktop
198+
199+ LOG.debug("sending dbus signal to Unity launcher for application: %r",
200 launcher_info.name)
201- LOG.debug(" launcher_info.icon_file_path: ",
202+ LOG.debug(" launcher_info: icon_file_path: %r ",
203 launcher_info.icon_file_path)
204- LOG.debug(" launcher_info.installed_desktop_file_path: ",
205+ LOG.debug(" launcher_info.installed_desktop_file_path: %r",
206 launcher_info.installed_desktop_file_path)
207- LOG.debug(" launcher_info.trans_id: ", launcher_info.trans_id)
208+ LOG.debug(" launcher_info.trans_id: %r", launcher_info.trans_id)
209+ LOG.debug(" launcher_info.icon_x: %r icon_y: %r",
210+ launcher_info.icon_x, launcher_info.icon_y)
211
212 try:
213- bus = dbus.SessionBus()
214- launcher_obj = bus.get_object('com.canonical.Unity.Launcher',
215- '/com/canonical/Unity/Launcher')
216- launcher_iface = dbus.Interface(launcher_obj,
217- 'com.canonical.Unity.Launcher')
218+ launcher_iface = self._get_launcher_dbus_iface()
219 launcher_iface.AddLauncherItemFromPosition(
220 launcher_info.name,
221 launcher_info.icon_file_path,
222
223=== added file 'softwarecenter/backend/zeitgeist_logger.py'
224--- softwarecenter/backend/zeitgeist_logger.py 1970-01-01 00:00:00 +0000
225+++ softwarecenter/backend/zeitgeist_logger.py 2013-09-17 17:52:06 +0000
226@@ -0,0 +1,97 @@
227+# Copyright (C) 2013 Canonical
228+#
229+# Authors:
230+# Marco Trevisan <marco.trevisan@canonical.com>
231+#
232+# This program is free software; you can redistribute it and/or modify it under
233+# the terms of the GNU General Public License as published by the Free Software
234+# Foundation; version 3.
235+#
236+# This program is distributed in the hope that it will be useful, but WITHOUT
237+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
238+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
239+# details.
240+#
241+# You should have received a copy of the GNU General Public License along with
242+# this program; if not, write to the Free Software Foundation, Inc.,
243+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
244+
245+import logging
246+from softwarecenter.utils import get_desktop_id
247+
248+LOG = logging.getLogger(__name__)
249+APPLICATION_URI_PREFIX = "application://"
250+HAVE_MODULE = False
251+
252+try:
253+ from zeitgeist.client import ZeitgeistClient
254+ from zeitgeist import datamodel as ZeitgeistDataModel
255+ from zeitgeist.datamodel import (Event as ZeitgeistEvent,
256+ Subject as ZeitgeistSubject)
257+ HAVE_MODULE = True
258+except ImportError:
259+ LOG.warn("Support for Zeitgeist disabled")
260+
261+class ZeitgeistLogger(object):
262+ def __init__(self, distro):
263+ self.distro = distro
264+
265+ def __create_user_event(self):
266+ event = ZeitgeistEvent()
267+ event.actor = APPLICATION_URI_PREFIX + self.distro.get_app_id() + ".desktop"
268+ event.manifestation = ZeitgeistDataModel.Manifestation.EVENT_MANIFESTATION.USER_ACTIVITY
269+ return event
270+
271+ def __create_app_subject(self, desktop_file):
272+ subject = ZeitgeistSubject()
273+ subject.interpretation = ZeitgeistDataModel.Interpretation.SOFTWARE
274+ subject.manifestation = ZeitgeistDataModel.Manifestation.SOFTWARE_ITEM
275+ subject.uri = APPLICATION_URI_PREFIX + get_desktop_id(desktop_file);
276+ subject.current_uri = subject.uri
277+ subject.mimetype = "application/x-desktop"
278+ return subject
279+
280+ def log_install_event(self, desktop_file):
281+ """Logs an install event on Zeitgeist"""
282+ if not HAVE_MODULE:
283+ LOG.warn("No zeitgeist support, impossible to log event")
284+ return False
285+
286+ if not desktop_file or not len(desktop_file):
287+ LOG.warn("Invalid desktop file provided, impossible to log event")
288+ return False
289+
290+ subject = self.__create_app_subject(desktop_file)
291+
292+ subject.text = "Installed with " + self.distro.get_app_name()
293+ event = self.__create_user_event()
294+ event.interpretation = ZeitgeistDataModel.Interpretation.EVENT_INTERPRETATION.CREATE_EVENT
295+ event.append_subject(subject)
296+ ZeitgeistClient().insert_event(event)
297+
298+ subject.text = "Accessed by " + self.distro.get_app_name()
299+ event = self.__create_user_event()
300+ event.interpretation = ZeitgeistDataModel.Interpretation.EVENT_INTERPRETATION.ACCESS_EVENT
301+ event.append_subject(subject)
302+ ZeitgeistClient().insert_event(event)
303+ return True
304+
305+ def log_uninstall_event(self, desktop_file):
306+ """Logs an uninstall event on Zeitgeist"""
307+ if not HAVE_MODULE:
308+ LOG.warn("No zeitgeist support, impossible to log event")
309+ return False
310+
311+ if not desktop_file or not len(desktop_file):
312+ LOG.warn("Invalid desktop file provided, impossible to log event")
313+ return False
314+
315+ subject = self.__create_app_subject(desktop_file)
316+ subject.text = "Uninstalled with " + self.distro.get_app_name()
317+
318+ event = self.__create_user_event()
319+ event.interpretation = ZeitgeistDataModel.Interpretation.EVENT_INTERPRETATION.DELETE_EVENT
320+ event.append_subject(subject)
321+ ZeitgeistClient().insert_event(event)
322+ return True
323+
324
325=== modified file 'softwarecenter/db/categories.py'
326--- softwarecenter/db/categories.py 2012-12-14 16:44:25 +0000
327+++ softwarecenter/db/categories.py 2013-09-17 17:52:06 +0000
328@@ -291,7 +291,9 @@
329
330 # debug print
331 for cat in categories:
332- LOG.debug("%s %s %s" % (cat.name, cat.iconname, cat.query))
333+ LOG.debug("%s %s %s" % (cat.name.decode('utf8'),
334+ cat.iconname,
335+ cat.query))
336 return categories
337
338 def _build_string_template_dict(self):
339
340=== modified file 'softwarecenter/db/database.py'
341--- softwarecenter/db/database.py 2012-12-14 16:44:25 +0000
342+++ softwarecenter/db/database.py 2013-09-17 17:52:06 +0000
343@@ -637,7 +637,7 @@
344 yield doc
345
346
347-class FakeMSetItem():
348+class FakeMSetItem(object):
349 def __init__(self, doc):
350 self.document = doc
351
352
353=== modified file 'softwarecenter/distro/__init__.py'
354--- softwarecenter/distro/__init__.py 2012-11-28 16:58:59 +0000
355+++ softwarecenter/distro/__init__.py 2013-09-17 17:52:06 +0000
356@@ -59,6 +59,12 @@
357 """
358 return _("Software Center")
359
360+ def get_app_id(self):
361+ """
362+ The application-id (as used on the .desktop file)
363+ """
364+ return "software-center"
365+
366 def get_app_description(self):
367 """
368 The description of the application displayed in the about dialog
369
370=== modified file 'softwarecenter/distro/fedora.py'
371--- softwarecenter/distro/fedora.py 2012-11-28 16:58:59 +0000
372+++ softwarecenter/distro/fedora.py 2013-09-17 17:52:06 +0000
373@@ -69,6 +69,9 @@
374 def get_app_name(self):
375 return _("Fedora Software Center")
376
377+ def get_app_id(self):
378+ return "fedora-software-center"
379+
380 def get_removal_warning_text(self, cache, pkg, appname, depends):
381 primary = _("To remove %s, these items must be removed "
382 "as well:") % appname
383
384=== modified file 'softwarecenter/distro/ubuntu.py'
385--- softwarecenter/distro/ubuntu.py 2013-05-01 18:13:43 +0000
386+++ softwarecenter/distro/ubuntu.py 2013-09-17 17:52:06 +0000
387@@ -80,6 +80,9 @@
388 def get_app_name(self):
389 return _("Ubuntu Software Center")
390
391+ def get_app_id(self):
392+ return "ubuntu-software-center"
393+
394 def get_app_description(self):
395 return _("Lets you choose from thousands of applications available "
396 "for Ubuntu.")
397
398=== removed directory 'softwarecenter/sso'
399=== removed file 'softwarecenter/sso/__init__.py'
400--- softwarecenter/sso/__init__.py 2012-06-25 21:26:13 +0000
401+++ softwarecenter/sso/__init__.py 1970-01-01 00:00:00 +0000
402@@ -1,18 +0,0 @@
403-# -*- coding: utf-8 -*-
404-#
405-# Copyright 2009-2012 Canonical Ltd.
406-#
407-# This program is free software: you can redistribute it and/or modify it
408-# under the terms of the GNU General Public License version 3, as published
409-# by the Free Software Foundation.
410-#
411-# This program is distributed in the hope that it will be useful, but
412-# WITHOUT ANY WARRANTY; without even the implied warranties of
413-# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
414-# PURPOSE. See the GNU General Public License for more details.
415-#
416-# You should have received a copy of the GNU General Public License along
417-# with this program. If not, see <http://www.gnu.org/licenses/>.
418-#
419-
420-"""Ubuntu Single Sign On GTK+ graphical interface."""
421
422=== removed file 'softwarecenter/sso/gui.py'
423--- softwarecenter/sso/gui.py 2012-11-28 17:05:41 +0000
424+++ softwarecenter/sso/gui.py 1970-01-01 00:00:00 +0000
425@@ -1,1168 +0,0 @@
426-# -*- coding: utf-8 -*-
427-#
428-# Copyright 2010-2012 Canonical Ltd.
429-#
430-# This program is free software: you can redistribute it and/or modify it
431-# under the terms of the GNU General Public License version 3, as published
432-# by the Free Software Foundation.
433-#
434-# This program is distributed in the hope that it will be useful, but
435-# WITHOUT ANY WARRANTY; without even the implied warranties of
436-# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
437-# PURPOSE. See the GNU General Public License for more details.
438-#
439-# You should have received a copy of the GNU General Public License along
440-# with this program. If not, see <http://www.gnu.org/licenses/>.
441-#
442-
443-"""The Ubuntu Single Sign On GTK+ graphical user interface."""
444-
445-import logging
446-import os
447-import sys
448-import tempfile
449-import webbrowser
450-
451-from functools import wraps, partial
452-
453-import dbus
454-
455-# pylint: disable=E0611,F0401
456-from gi.repository import Gdk, Gtk
457-from gi.repository.GdkX11 import X11Window
458-# pylint: enable=E0611,F0401
459-
460-from ubuntu_sso import (
461- DBUS_BUS_NAME,
462- DBUS_ACCOUNT_PATH,
463- DBUS_IFACE_USER_NAME,
464- NO_OP,
465- USER_CANCELLATION,
466- USER_SUCCESS,
467-)
468-from ubuntu_sso.logger import setup_gui_logging
469-from ubuntu_sso.utils import ui as ui_strings
470-from ubuntu_sso.utils.ui import (
471- CAPTCHA_LOAD_ERROR,
472- CAPTCHA_RELOAD_TOOLTIP,
473- CONNECT_HELP_LABEL,
474- EMAIL_MISMATCH,
475- EMAIL_INVALID,
476- ERROR,
477- FIELD_REQUIRED,
478- FORGOTTEN_PASSWORD_BUTTON,
479- GENERIC_BACKEND_ERROR,
480- is_min_required_password,
481- is_correct_email,
482- JOIN_HEADER_LABEL,
483- LOADING,
484- LOGIN_BUTTON_LABEL,
485- LOGIN_HEADER_LABEL,
486- NEXT,
487- ONE_MOMENT_PLEASE,
488- PASSWORD_CHANGED,
489- PASSWORD_HELP,
490- PASSWORD_MISMATCH,
491- PASSWORD_TOO_WEAK,
492- REQUEST_PASSWORD_TOKEN_LABEL,
493- RESET_PASSWORD,
494- SET_NEW_PASSWORD_LABEL,
495- SUCCESS,
496- TC_BUTTON,
497- TC_NOT_ACCEPTED,
498- VERIFY_EMAIL_LABEL,
499- YES_TO_TC,
500- YES_TO_UPDATES,
501-)
502-
503-# Instance of 'UbuntuSSOClientGUI' has no 'yyy' member
504-# pylint: disable=E1101
505-
506-
507-logger = setup_gui_logging('ubuntu_sso.gui.gtk')
508-
509-
510-# pylint: disable=C0103
511-def parse_color(color):
512- """Parse a string color into Gdk.Color."""
513- c = Gdk.RGBA()
514- result = c.parse(color)
515- if not result:
516- logger.warning('Could not parse color %r.', color)
517- return c
518-# pylint: enable=C0103
519-
520-DEFAULT_WIDTH = 30
521-# To be replaced by values from the theme (LP: #616526)
522-HELP_TEXT_COLOR = parse_color("#bfbfbf")
523-WARNING_TEXT_COLOR = parse_color("red")
524-LARGE_MARKUP = u'<span size="x-large">%s</span>'
525-
526-
527-# SSL properties and certs location
528-STRICT_SSL_PROP = 'ssl-strict'
529-CERTS_FILE_PROP = 'ssl-ca-file'
530-CA_CERT_FILE = '/etc/ssl/certs/ca-certificates.crt'
531-
532-
533-def log_call(f):
534- """Decorator to log call functions."""
535-
536- @wraps(f)
537- def inner(*args, **kwargs):
538- """Execute 'f' logging the call as INFO."""
539- logger.info('%s: args %r, kwargs %r.', f.__name__, args, kwargs)
540- return f(*args, **kwargs)
541-
542- return inner
543-
544-
545-def get_sso_client():
546- bus = dbus.SessionBus()
547- obj = bus.get_object(bus_name=DBUS_BUS_NAME,
548- object_path=DBUS_ACCOUNT_PATH,
549- follow_name_owner_changes=True)
550- result = dbus.Interface(obj, dbus_interface=DBUS_IFACE_USER_NAME)
551- result.disconnect_from_signal = lambda _, sig: sig.remove()
552- return result
553-
554-
555-def get_data_file(*args):
556- result = os.path.abspath(os.path.join(os.path.dirname(__file__),
557- '..', '..', 'data'))
558- if not os.path.exists(result):
559- import softwarecenter.paths
560- result = softwarecenter.paths.datadir
561-
562- result = os.path.join(result, 'ui', 'sso', *args)
563- logger.info('Using data dir: %r', result)
564- return result
565-
566-
567-class LabeledEntry(Gtk.Entry):
568- """An entry that displays the label within itself in a grey color."""
569-
570- # Use of super on an old style class
571- # pylint: disable=E1002
572-
573- def __init__(self, label, is_password=False, *args, **kwargs):
574- self.label = label
575- self.is_password = is_password
576- self.warning = None
577-
578- super(LabeledEntry, self).__init__(*args, **kwargs)
579-
580- self.set_width_chars(DEFAULT_WIDTH)
581- self._set_label(self, None)
582- self.set_tooltip_text(self.label)
583- self.connect('focus-in-event', self._clear_text)
584- self.connect('focus-out-event', self._set_label)
585- self.clear_warning()
586- self.show()
587-
588- def _clear_text(self, *args, **kwargs):
589- """Clear text and restore text color."""
590- self.set_text(self.get_text())
591-
592- # restore to theme's default
593- self.override_color(Gtk.StateFlags.NORMAL, None)
594-
595- if self.is_password:
596- self.set_visibility(False)
597-
598- return False # propagate the event further
599-
600- def _set_label(self, *args, **kwargs):
601- """Set the proper label and proper coloring."""
602- if self.get_text():
603- return
604-
605- self.set_text(self.label)
606- self.override_color(Gtk.StateFlags.NORMAL, HELP_TEXT_COLOR)
607-
608- if self.is_password:
609- self.set_visibility(True)
610-
611- return False # propagate the event further
612-
613- def get_text(self):
614- """Get text only if it's not the label nor empty."""
615- result = super(LabeledEntry, self).get_text().decode('utf8')
616- if result == self.label or result.isspace():
617- result = u''
618- return result
619-
620- def set_warning(self, warning_msg):
621- """Display warning as secondary icon, set 'warning_msg' as tooltip."""
622- self.warning = warning_msg
623- self.set_property('secondary-icon-stock', Gtk.STOCK_DIALOG_WARNING)
624- self.set_property('secondary-icon-sensitive', True)
625- self.set_property('secondary-icon-activatable', False)
626- self.set_property('secondary-icon-tooltip-text', warning_msg)
627-
628- def clear_warning(self):
629- """Remove any warning."""
630- self.warning = None
631- self.set_property('secondary-icon-stock', None)
632- self.set_property('secondary-icon-sensitive', False)
633- self.set_property('secondary-icon-activatable', False)
634- self.set_property('secondary-icon-tooltip-text', None)
635-
636-
637-class UbuntuSSOClientGUI(object):
638- """Ubuntu single sign-on GUI."""
639-
640- def __init__(self, app_name, **kwargs):
641- """Create the GUI and initialize widgets."""
642- logger.debug('UbuntuSSOClientGUI: app_name %r, kwargs %r.',
643- app_name, kwargs)
644-
645- self._captcha_filename = tempfile.mktemp()
646- self._captcha_id = None
647- self._signals_receivers = {}
648- self._done = False # whether the whole process was completed or not
649-
650- self.app_name = app_name
651- self.app_label = u'<b>%s</b>' % self.app_name
652- self.ping_url = kwargs.get('ping_url', u'')
653- self.tc_url = kwargs.get('tc_url', u'')
654- self.help_text = kwargs.get('help_text', u'')
655- self.login_only = kwargs.get('login_only', False)
656- window_id = kwargs.get('window_id', 0)
657- self.close_callback = kwargs.get('close_callback', NO_OP)
658- self.backend = None
659- self.user_email = None
660- self.user_password = None
661-
662- ui_filename = get_data_file('sso.ui')
663- builder = Gtk.Builder()
664- builder.add_from_file(ui_filename)
665- builder.connect_signals(self)
666-
667- self.widgets = []
668- self.warnings = []
669- self.cancels = []
670- for obj in builder.get_objects():
671- name = getattr(obj, 'name', None)
672- if name is None and isinstance(obj, Gtk.Buildable):
673- # work around bug lp:507739
674- name = Gtk.Buildable.get_name(obj)
675- if name is None:
676- logging.warn("%s has no name (??)", obj)
677- else:
678- self.widgets.append(name)
679- setattr(self, name, obj)
680- if 'warning' in name:
681- self.warnings.append(obj)
682- obj.set_text('')
683- if 'cancel_button' in name:
684- obj.connect('clicked', self.on_close_clicked)
685- self.cancels.append(obj)
686-
687- # Connect the activate-link signal here
688- # GtkBuilder in GTK 3 seems to not do this
689- self.login_button.connect('activate-link', self.on_activate_link)
690- self.forgotten_password_button.connect('activate-link',
691- self.on_activate_link)
692-
693- self.entries = (u'name_entry', u'email1_entry', u'email2_entry',
694- u'password1_entry', u'password2_entry',
695- u'captcha_solution_entry', u'email_token_entry',
696- u'login_email_entry', u'login_password_entry',
697- u'reset_email_entry', u'reset_code_entry',
698- u'reset_password1_entry', u'reset_password2_entry')
699-
700- for name in self.entries:
701- label = getattr(ui_strings, name.upper())
702- is_password = 'password' in name
703- entry = LabeledEntry(label=label, is_password=is_password)
704- entry.set_activates_default(True)
705- setattr(self, name, entry)
706-
707- self.window.set_icon_name('ubuntu-logo')
708-
709- self.pages = (self.enter_details_vbox, self.processing_vbox,
710- self.verify_email_vbox, self.finish_vbox,
711- self.tc_browser_vbox, self.login_vbox,
712- self.request_password_token_vbox,
713- self.set_new_password_vbox)
714-
715- self._signals = {
716- 'CaptchaGenerated':
717- self._filter_by_app_name(self.on_captcha_generated),
718- 'CaptchaGenerationError':
719- self._filter_by_app_name(self.on_captcha_generation_error),
720- 'UserRegistered':
721- self._filter_by_app_name(self.on_user_registered),
722- 'UserRegistrationError':
723- self._filter_by_app_name(self.on_user_registration_error),
724- 'EmailValidated':
725- self._filter_by_app_name(self.on_email_validated),
726- 'EmailValidationError':
727- self._filter_by_app_name(self.on_email_validation_error),
728- 'LoggedIn':
729- self._filter_by_app_name(self.on_logged_in),
730- 'LoginError':
731- self._filter_by_app_name(self.on_login_error),
732- 'UserNotValidated':
733- self._filter_by_app_name(self.on_user_not_validated),
734- 'PasswordResetTokenSent':
735- self._filter_by_app_name(self.on_password_reset_token_sent),
736- 'PasswordResetError':
737- self._filter_by_app_name(self.on_password_reset_error),
738- 'PasswordChanged':
739- self._filter_by_app_name(self.on_password_changed),
740- 'PasswordChangeError':
741- self._filter_by_app_name(self.on_password_change_error),
742- }
743-
744- if window_id != 0:
745- # be as robust as possible:
746- # if the window_id is not "good", set_transient_for will fail
747- # awfully, and we don't want that: if the window_id is bad we can
748- # still do everything as a standalone window. Also,
749- # window_foreign_new may return None breaking set_transient_for.
750- try:
751- display = Gdk.Display.get_default()
752- # this is not working, we need to create a XLib.window
753- # as a second parameter to foreign_new_for_display
754- win = X11Window.foreign_new_for_display(display, None)
755- self.window.realize()
756- self.window.window.set_transient_for(win)
757- except: # pylint: disable=W0702
758- msg = 'UbuntuSSOClientGUI: failed set_transient_for win id %r'
759- logger.exception(msg, window_id)
760-
761- self.yes_to_updates_checkbutton.hide()
762- self.start_backend()
763-
764- def start_backend(self):
765- """Start the backend, show the window when ready."""
766- self.backend = get_sso_client()
767-
768- logger.debug('UbuntuSSOClientGUI: backend created: %r', self.backend)
769-
770- self._setup_signals()
771- self._append_pages()
772- self.window.show()
773-
774- @property
775- def success_vbox(self):
776- """The success page."""
777- message = SUCCESS % {'app_name': self.app_name}
778- message = LARGE_MARKUP % message
779- self.finish_vbox.label.set_markup(message)
780- return self.finish_vbox
781-
782- @property
783- def error_vbox(self):
784- """The error page."""
785- self.finish_vbox.label.set_markup(LARGE_MARKUP % ERROR)
786- return self.finish_vbox
787-
788- # helpers
789-
790- def _filter_by_app_name(self, f):
791- """Execute the decorated function only for 'self.app_name'."""
792-
793- @wraps(f)
794- def inner(app_name, *args, **kwargs):
795- """Execute 'f' only if 'app_name' matches 'self.app_name'."""
796- result = None
797- if app_name == self.app_name:
798- result = f(app_name, *args, **kwargs)
799- else:
800- logger.info('%s: ignoring call since received app_name '
801- '%r (expected %r)',
802- f.__name__, app_name, self.app_name)
803- return result
804-
805- return inner
806-
807- def _setup_signals(self):
808- """Bind signals to callbacks to be able to test the pages."""
809- for signal, method in self._signals.items():
810- actual = self._signals_receivers.get(signal)
811- if actual is not None:
812- msg = 'Signal %r is already connected with %r.'
813- logger.warning(msg, signal, actual)
814-
815- match = self.backend.connect_to_signal(signal, method)
816- self._signals_receivers[signal] = match
817-
818- def _add_spinner_to_container(self, container, legend=None):
819- """Add a spinner to 'container'."""
820- spinner = Gtk.Spinner()
821- spinner.start()
822-
823- label = Gtk.Label()
824- if legend:
825- label.set_text(legend)
826- else:
827- label.set_text(LOADING)
828-
829- hbox = Gtk.HBox(spacing=5)
830- hbox.pack_start(spinner, expand=False, fill=True, padding=0)
831- hbox.pack_start(label, expand=False, fill=True, padding=0)
832-
833- alignment = Gtk.Alignment(xalign=0.5, yalign=0.5,
834- xscale=0, yscale=0)
835- alignment.add(hbox)
836- alignment.show_all()
837-
838- # remove children to avoid:
839- # GtkWarning: Attempting to add a widget with type GtkAlignment to a
840- # GtkEventBox, but as a GtkBin subclass a GtkEventBox can only contain
841- # one widget at a time
842- for child in container.get_children():
843- container.remove(child)
844- container.add(alignment)
845-
846- def _set_warning_message(self, widget, message):
847- """Set 'message' as text for 'widget'."""
848- widget.set_text(message)
849- widget.override_color(Gtk.StateFlags.NORMAL, WARNING_TEXT_COLOR)
850- widget.show()
851-
852- def _clear_warnings(self):
853- """Clear all warning messages."""
854- for widget in self.warnings:
855- widget.set_text('')
856- for widget in self.entries:
857- getattr(self, widget).clear_warning()
858-
859- def _non_empty_input(self, widget):
860- """Return weather widget has non empty content."""
861- text = widget.get_text()
862- return bool(text and not text.isspace())
863-
864- def _handle_error(self, remote_call, handler, error):
865- """Handle any error when calling the remote backend."""
866- logger.error('Remote call %r failed with: %r', remote_call, error)
867- errordict = {'message': GENERIC_BACKEND_ERROR}
868- handler(self.app_name, errordict)
869-
870- # build pages
871-
872- def _append_pages(self):
873- """Append all the requires pages to main widget."""
874- self._append_page(self._build_processing_page())
875- self._append_page(self._build_finish_page())
876- self._append_page(self._build_login_page())
877- self._append_page(self._build_request_password_token_page())
878- self._append_page(self._build_set_new_password_page())
879- self._append_page(self._build_verify_email_page())
880-
881- if not self.login_only:
882- self._append_page(self._build_enter_details_page())
883- self._append_page(self._build_tc_page())
884- self.login_button.grab_focus()
885- self._set_current_page(self.enter_details_vbox)
886- else:
887- self.login_back_button.hide()
888- self.login_ok_button.grab_focus()
889- self.login_vbox.help_text = self.help_text
890- self._set_current_page(self.login_vbox)
891-
892- def _append_page(self, page):
893- """Append 'page' to the 'window'."""
894- self.content.append_page(page, None)
895-
896- def _set_header(self, header):
897- """Set 'header' as the window title and header."""
898- self.header_label.set_markup(LARGE_MARKUP % header)
899- self.window.set_title(self.header_label.get_text()) # avoid markup
900-
901- def _set_current_page(self, current_page, warning_text=None):
902- """Hide all the pages but 'current_page'."""
903- if hasattr(current_page, 'header'):
904- self._set_header(current_page.header)
905-
906- if hasattr(current_page, 'help_text'):
907- self.help_label.set_markup(current_page.help_text)
908-
909- if warning_text is not None:
910- self._set_warning_message(self.warning_label, warning_text)
911- else:
912- self.warning_label.set_text('')
913-
914- self.content.set_current_page(self.content.page_num(current_page))
915-
916- if current_page.default_widget is not None:
917- current_page.default_widget.grab_default()
918-
919- def _generate_captcha(self):
920- """Ask for a new captcha; update the ui to reflect the fact."""
921- logger.info('Calling generate_captcha with filename path at %r',
922- self._captcha_filename)
923- self.warning_label.set_text('')
924- f = self.backend.generate_captcha
925- error_handler = partial(self._handle_error, f,
926- self.on_captcha_generation_error)
927- f(self.app_name, self._captcha_filename,
928- reply_handler=NO_OP, error_handler=error_handler)
929- self._set_captcha_loading()
930-
931- def _set_captcha_loading(self):
932- """Present a spinner to the user while the captcha is downloaded."""
933- self.captcha_image.hide()
934- self._add_spinner_to_container(self.captcha_loading)
935- self.captcha_loading.override_background_color(Gtk.StateFlags.NORMAL,
936- parse_color('white'))
937- self.captcha_loading.show_all()
938- self.join_ok_button.set_sensitive(False)
939-
940- def _set_captcha_image(self):
941- """Present a captcha image to the user to be resolved."""
942- self.captcha_loading.hide()
943- self.join_ok_button.set_sensitive(True)
944- self.captcha_image.set_from_file(self._captcha_filename)
945- self.captcha_image.show()
946-
947- def _build_enter_details_page(self):
948- """Build the enter details page."""
949- d = {'app_name': self.app_label}
950- self.enter_details_vbox.header = JOIN_HEADER_LABEL % d
951- self.enter_details_vbox.help_text = self.help_text
952- self.enter_details_vbox.default_widget = self.join_ok_button
953- self.join_ok_button.set_can_default(True)
954-
955- self.enter_details_vbox.pack_start(self.name_entry,
956- expand=False, fill=True, padding=0)
957- self.enter_details_vbox.reorder_child(self.name_entry, 0)
958- entry = self.captcha_solution_entry
959- self.captcha_solution_vbox.pack_start(entry,
960- expand=False, fill=True, padding=0)
961- msg = CAPTCHA_RELOAD_TOOLTIP
962- self.captcha_reload_button.set_tooltip_text(msg)
963-
964- self.emails_hbox.pack_start(self.email1_entry,
965- expand=False, fill=True, padding=0)
966- self.emails_hbox.pack_start(self.email2_entry,
967- expand=False, fill=True, padding=0)
968-
969- self.passwords_hbox.pack_start(self.password1_entry,
970- expand=False, fill=True, padding=0)
971- self.passwords_hbox.pack_start(self.password2_entry,
972- expand=False, fill=True, padding=0)
973- help_msg = '<small>%s</small>' % PASSWORD_HELP
974- self.password_help_label.set_markup(help_msg)
975-
976- if not os.path.exists(self._captcha_filename):
977- self._generate_captcha()
978- else:
979- self._set_captcha_image()
980-
981- msg = YES_TO_UPDATES % {'app_name': self.app_name}
982- self.yes_to_updates_checkbutton.set_label(msg)
983-
984- msg = YES_TO_TC % {'app_name': self.app_name}
985- self.yes_to_tc_checkbutton.set_label(msg)
986- self.tc_button.set_label(TC_BUTTON)
987-
988- if not self.tc_url:
989- self.tc_vbox.hide()
990- self.login_button.set_label(LOGIN_BUTTON_LABEL)
991-
992- return self.enter_details_vbox
993-
994- def _build_tc_page(self):
995- """Build the Terms & Conditions page."""
996- self.tc_browser_vbox.help_text = ''
997- self.tc_browser_vbox.default_widget = self.tc_back_button
998- self.tc_browser_vbox.default_widget.set_can_default(True)
999- return self.tc_browser_vbox
1000-
1001- def _build_processing_page(self):
1002- """Build the processing page with a spinner."""
1003- self.processing_vbox.default_widget = None
1004- self._add_spinner_to_container(self.processing_vbox,
1005- legend=ONE_MOMENT_PLEASE)
1006- return self.processing_vbox
1007-
1008- def _build_verify_email_page(self):
1009- """Build the verify email page."""
1010- self.verify_email_vbox.default_widget = self.verify_token_button
1011- self.verify_email_vbox.default_widget.set_can_default(True)
1012-
1013- self.verify_email_details_vbox.pack_start(self.email_token_entry,
1014- expand=False, fill=True, padding=0)
1015- return self.verify_email_vbox
1016-
1017- def _build_finish_page(self):
1018- """Build the success page."""
1019- self.finish_vbox.default_widget = self.finish_close_button
1020- self.finish_vbox.default_widget.set_can_default(True)
1021- self.finish_vbox.label = self.finish_label
1022- return self.finish_vbox
1023-
1024- def _build_login_page(self):
1025- """Build the login page."""
1026- d = {'app_name': self.app_label}
1027- self.login_vbox.header = LOGIN_HEADER_LABEL % d
1028- self.login_vbox.help_text = CONNECT_HELP_LABEL % d
1029- self.login_vbox.default_widget = self.login_ok_button
1030- self.login_vbox.default_widget.set_can_default(True)
1031-
1032- self.login_details_vbox.pack_start(self.login_email_entry,
1033- expand=True, fill=True, padding=0)
1034- self.login_details_vbox.reorder_child(self.login_email_entry, 0)
1035- self.login_details_vbox.pack_start(self.login_password_entry,
1036- expand=True, fill=True, padding=0)
1037- self.login_details_vbox.reorder_child(self.login_password_entry, 1)
1038-
1039- msg = FORGOTTEN_PASSWORD_BUTTON
1040- self.forgotten_password_button.set_label(msg)
1041- self.login_ok_button.grab_focus()
1042-
1043- return self.login_vbox
1044-
1045- def _build_request_password_token_page(self):
1046- """Build the login page."""
1047- self.request_password_token_vbox.header = RESET_PASSWORD
1048- text = REQUEST_PASSWORD_TOKEN_LABEL % {'app_name': self.app_label}
1049- self.request_password_token_vbox.help_text = text
1050- btn = self.request_password_token_ok_button
1051- btn.set_can_default(True)
1052- self.request_password_token_vbox.default_widget = btn
1053-
1054- entry = self.reset_email_entry
1055- self.request_password_token_details_vbox.pack_start(entry,
1056- expand=False, fill=True, padding=0)
1057- cb = self.on_reset_email_entry_changed
1058- self.reset_email_entry.connect('changed', cb)
1059- self.request_password_token_ok_button.set_label(NEXT)
1060- self.request_password_token_ok_button.set_sensitive(False)
1061-
1062- return self.request_password_token_vbox
1063-
1064- def _build_set_new_password_page(self):
1065- """Build the login page."""
1066- self.set_new_password_vbox.header = RESET_PASSWORD
1067- self.set_new_password_vbox.help_text = SET_NEW_PASSWORD_LABEL
1068- btn = self.set_new_password_ok_button
1069- btn.set_can_default(True)
1070- self.set_new_password_vbox.default_widget = btn
1071-
1072- for entry in (self.reset_code_entry,
1073- self.reset_password1_entry,
1074- self.reset_password2_entry):
1075- self.set_new_password_details_vbox.pack_start(entry,
1076- expand=False, fill=True, padding=0)
1077-
1078- cb = self.on_set_new_password_entries_changed
1079- self.reset_code_entry.connect('changed', cb)
1080- self.reset_password1_entry.connect('changed', cb)
1081- self.reset_password2_entry.connect('changed', cb)
1082- help_msg = '<small>%s</small>' % PASSWORD_HELP
1083- self.reset_password_help_label.set_markup(help_msg)
1084-
1085- self.set_new_password_ok_button.set_label(RESET_PASSWORD)
1086- self.set_new_password_ok_button.set_sensitive(False)
1087-
1088- return self.set_new_password_vbox
1089-
1090- def _validate_email(self, email1, email2=None):
1091- """Validate 'email1', return error message if not valid.
1092-
1093- If 'email2' is given, must match 'email1'.
1094- """
1095- if email2 is not None and email1 != email2:
1096- return EMAIL_MISMATCH
1097-
1098- if not email1:
1099- return FIELD_REQUIRED
1100-
1101- if not is_correct_email(email1):
1102- return EMAIL_INVALID
1103-
1104- def _validate_password(self, password1, password2=None):
1105- """Validate 'password1', return error message if not valid.
1106-
1107- If 'password2' is given, must match 'email1'.
1108- """
1109- if password2 is not None and password1 != password2:
1110- return PASSWORD_MISMATCH
1111-
1112- if not is_min_required_password(password1):
1113- return PASSWORD_TOO_WEAK
1114-
1115- # GTK callbacks
1116-
1117- def destroy(self):
1118- """Destroy this UI."""
1119- self.window.hide()
1120- self.window.destroy()
1121-
1122- def connect(self, signal_name, handler, *args, **kwargs):
1123- """Connect 'signal_name' with 'handler'."""
1124- logger.debug('connect: signal %r, handler %r, args %r, kwargs, %r',
1125- signal_name, handler, args, kwargs)
1126- self.window.connect(signal_name, handler, *args, **kwargs)
1127-
1128- def finish_success(self):
1129- """The whole process was completed successfully. Show success page."""
1130- self._done = True
1131- self._set_current_page(self.success_vbox)
1132-
1133- def finish_error(self):
1134- """The whole process wasn't completed successfully. Show error page."""
1135- self._done = True
1136- self._set_current_page(self.error_vbox)
1137-
1138- def on_activate_link(self, button):
1139- """Do nothing, used for LinkButtons that are used as regular ones."""
1140- return True
1141-
1142- def on_close_clicked(self, *args, **kwargs):
1143- """Call self.close_callback if defined."""
1144- if os.path.exists(self._captcha_filename):
1145- os.remove(self._captcha_filename)
1146-
1147- for signal, match in self._signals_receivers.items():
1148- self.backend.disconnect_from_signal(signal, match)
1149-
1150- # hide the main window
1151- if self.window is not None:
1152- self.window.hide()
1153-
1154- # process any pending events before callbacking with result
1155- while Gtk.events_pending():
1156- Gtk.main_iteration()
1157-
1158- return_code = USER_SUCCESS
1159- if not self._done:
1160- return_code = USER_CANCELLATION
1161- logger.info('Return code will be %r.', return_code)
1162-
1163- # call user defined callback
1164- logger.debug('Calling custom close_callback %r with params %r, %r',
1165- self.close_callback, args, kwargs)
1166- self.close_callback(*args, **kwargs)
1167-
1168- sys.exit(return_code)
1169-
1170- def on_sign_in_button_clicked(self, *args, **kwargs):
1171- """User wants to sign in, present the Login page."""
1172- self._set_current_page(self.login_vbox)
1173-
1174- def on_join_ok_button_clicked(self, *args, **kwargs):
1175- """Submit info for processing, present the processing vbox."""
1176- if not self.join_ok_button.is_sensitive():
1177- return
1178-
1179- self._clear_warnings()
1180-
1181- error = False
1182-
1183- name = self.name_entry.get_text()
1184- if not name:
1185- self.name_entry.set_warning(FIELD_REQUIRED)
1186- logger.warning('on_join_ok_button_clicked: name not set.')
1187- error = True
1188-
1189- # check email
1190- email1 = self.email1_entry.get_text()
1191- email2 = self.email2_entry.get_text()
1192- msg = self._validate_email(email1, email2)
1193- if msg is not None:
1194- self.email1_entry.set_warning(msg)
1195- self.email2_entry.set_warning(msg)
1196- logger.warning('on_join_ok_button_clicked: email is not valid.')
1197- error = True
1198-
1199- # check password
1200- password1 = self.password1_entry.get_text()
1201- password2 = self.password2_entry.get_text()
1202- msg = self._validate_password(password1, password2)
1203- if msg is not None:
1204- self.password1_entry.set_warning(msg)
1205- self.password2_entry.set_warning(msg)
1206- logger.warning('on_join_ok_button_clicked: password is not valid.')
1207- error = True
1208-
1209- # check T&C
1210- if self.tc_url and not self.yes_to_tc_checkbutton.get_active():
1211- self._set_warning_message(self.tc_warning_label,
1212- TC_NOT_ACCEPTED % {'app_name': self.app_name})
1213- logger.warning('on_join_ok_button_clicked: terms and conditions '
1214- 'not accepted.')
1215- error = True
1216-
1217- captcha_solution = self.captcha_solution_entry.get_text()
1218- if not captcha_solution:
1219- self.captcha_solution_entry.set_warning(FIELD_REQUIRED)
1220- logger.warning('on_join_ok_button_clicked: captcha solution not '
1221- 'set.')
1222- error = True
1223-
1224- if error:
1225- logger.warning('on_join_ok_button_clicked: validation failed.')
1226- return
1227-
1228- logger.info('on_join_ok_button_clicked: validation success!')
1229-
1230- self._set_current_page(self.processing_vbox)
1231- self.user_email = email1
1232- self.user_password = password1
1233-
1234- logger.info('Calling register_user with email %r, password <hidden>,'
1235- ' name %r, captcha_id %r and captcha_solution %r.', email1,
1236- name, self._captcha_id, captcha_solution)
1237-
1238- f = self.backend.register_user
1239- error_handler = partial(self._handle_error, f,
1240- self.on_user_registration_error)
1241- f(self.app_name, self.user_email, self.user_password, name,
1242- self._captcha_id, captcha_solution,
1243- reply_handler=NO_OP, error_handler=error_handler)
1244-
1245- def on_verify_token_button_clicked(self, *args, **kwargs):
1246- """The user entered the email token, let's verify!"""
1247- if not self.verify_token_button.is_sensitive():
1248- return
1249-
1250- self._clear_warnings()
1251-
1252- email_token = self.email_token_entry.get_text()
1253- if not email_token:
1254- self.email_token_entry.set_warning(FIELD_REQUIRED)
1255- return
1256-
1257- email = self.user_email
1258- password = self.user_password
1259-
1260- args = (self.app_name, email, password, email_token)
1261- if self.ping_url:
1262- f = self.backend.validate_email_and_ping
1263- args = args + (self.ping_url,)
1264- else:
1265- f = self.backend.validate_email
1266-
1267- logger.info('Calling validate_email with email %r, password <hidden>, '
1268- 'app_name %r and email_token %r.', email, self.app_name,
1269- email_token)
1270- error_handler = partial(self._handle_error, f,
1271- self.on_email_validation_error)
1272- f(*args, reply_handler=NO_OP, error_handler=error_handler)
1273-
1274- self._set_current_page(self.processing_vbox)
1275-
1276- def on_login_connect_button_clicked(self, *args, **kwargs):
1277- """User wants to connect!"""
1278- if not self.login_ok_button.is_sensitive():
1279- return
1280-
1281- self._clear_warnings()
1282-
1283- error = False
1284-
1285- email = self.login_email_entry.get_text()
1286- msg = self._validate_email(email)
1287- if msg is not None:
1288- self.login_email_entry.set_warning(msg)
1289- error = True
1290-
1291- password = self.login_password_entry.get_text()
1292- if not password:
1293- self.login_password_entry.set_warning(FIELD_REQUIRED)
1294- error = True
1295-
1296- if error:
1297- return
1298-
1299- args = (self.app_name, email, password)
1300- if self.ping_url:
1301- f = self.backend.login_and_ping
1302- args = args + (self.ping_url,)
1303- else:
1304- f = self.backend.login
1305-
1306- error_handler = partial(self._handle_error, f, self.on_login_error)
1307- f(*args, reply_handler=NO_OP, error_handler=error_handler)
1308-
1309- self._set_current_page(self.processing_vbox)
1310- self.user_email = email
1311- self.user_password = password
1312-
1313- def on_login_back_button_clicked(self, *args, **kwargs):
1314- """User wants to go to the previous page."""
1315- self._set_current_page(self.enter_details_vbox)
1316-
1317- def on_forgotten_password_button_clicked(self, *args, **kwargs):
1318- """User wants to reset the password."""
1319- self._set_current_page(self.request_password_token_vbox)
1320-
1321- def on_request_password_token_ok_button_clicked(self, *args, **kwargs):
1322- """User entered the email address to reset the password."""
1323- if not self.request_password_token_ok_button.is_sensitive():
1324- return
1325-
1326- self._clear_warnings()
1327-
1328- email = self.reset_email_entry.get_text()
1329- msg = self._validate_email(email)
1330- if msg is not None:
1331- self.reset_email_entry.set_warning(msg)
1332- return
1333-
1334- logger.info('Calling request_password_reset_token with %r.', email)
1335- f = self.backend.request_password_reset_token
1336- error_handler = partial(self._handle_error, f,
1337- self.on_password_reset_error)
1338- f(self.app_name, email,
1339- reply_handler=NO_OP, error_handler=error_handler)
1340-
1341- self._set_current_page(self.processing_vbox)
1342-
1343- def on_request_password_token_back_button_clicked(self, *args, **kwargs):
1344- """User wants to go to the previous page."""
1345- self._set_current_page(self.login_vbox)
1346-
1347- def on_reset_email_entry_changed(self, widget, *args, **kwargs):
1348- """User is changing the 'widget' entry in the reset email page."""
1349- sensitive = self._non_empty_input(widget)
1350- self.request_password_token_ok_button.set_sensitive(sensitive)
1351-
1352- def on_set_new_password_entries_changed(self, *args, **kwargs):
1353- """User is changing the 'widget' entry in the reset password page."""
1354- sensitive = True
1355- for entry in (self.reset_code_entry,
1356- self.reset_password1_entry,
1357- self.reset_password2_entry):
1358- sensitive &= self._non_empty_input(entry)
1359- self.set_new_password_ok_button.set_sensitive(sensitive)
1360-
1361- def on_set_new_password_ok_button_clicked(self, *args, **kwargs):
1362- """User entered reset code and new passwords."""
1363- if not self.set_new_password_ok_button.is_sensitive():
1364- return
1365-
1366- self._clear_warnings()
1367-
1368- error = False
1369-
1370- token = self.reset_code_entry.get_text()
1371- if not token:
1372- self.reset_code_entry.set_warning(FIELD_REQUIRED)
1373- error = True
1374-
1375- password1 = self.reset_password1_entry.get_text()
1376- password2 = self.reset_password2_entry.get_text()
1377- msg = self._validate_password(password1, password2)
1378- if msg is not None:
1379- self.reset_password1_entry.set_warning(msg)
1380- self.reset_password2_entry.set_warning(msg)
1381- error = True
1382-
1383- if error:
1384- return
1385-
1386- email = self.reset_email_entry.get_text()
1387- logger.info('Calling set_new_password with email %r, token %r and '
1388- 'new password: <hidden>.', email, token)
1389- f = self.backend.set_new_password
1390- error_handler = partial(self._handle_error, f,
1391- self.on_password_change_error)
1392- f(self.app_name, email, token, password1,
1393- reply_handler=NO_OP, error_handler=error_handler)
1394-
1395- self._set_current_page(self.processing_vbox)
1396-
1397- def _webkit_init_ssl(self):
1398- """Set the WebKit ssl strictness."""
1399- # delay the import of webkit to be able to build without it
1400- from gi.repository import WebKit # pylint: disable=E0611
1401-
1402- # Set the Soup session to be strict and use system CA certs
1403- session = WebKit.get_default_session()
1404- session.set_property(STRICT_SSL_PROP, True)
1405- session.set_property(CERTS_FILE_PROP, CA_CERT_FILE)
1406-
1407- def _add_webkit_browser(self):
1408- """Add the webkit browser for the t&c."""
1409- # delay the import of webkit to be able to build without it
1410- from gi.repository import WebKit # pylint: disable=E0611
1411-
1412- self._webkit_init_ssl()
1413-
1414- browser = WebKit.WebView()
1415-
1416- browser.connect('notify::load-status',
1417- self.on_tc_browser_notify_load_status)
1418- browser.connect('navigation-policy-decision-requested',
1419- self.on_tc_browser_navigation_requested)
1420-
1421- settings = browser.get_settings()
1422- settings.set_property("enable-plugins", False)
1423- settings.set_property("enable-default-context-menu", False)
1424-
1425- # webkit_web_view_open has been deprecated since version 1.1.1 and
1426- # should not be used in newly-written code. Use
1427- # webkit_web_view_load_uri() instead.
1428- browser.load_uri(self.tc_url)
1429- browser.show()
1430- self.tc_browser_window.add(browser)
1431-
1432- def on_tc_button_clicked(self, *args, **kwargs):
1433- """The T&C button was clicked, create the browser and load terms."""
1434- if self.tc_browser_window.get_child() is None:
1435- self._add_webkit_browser()
1436- self._set_current_page(self.processing_vbox)
1437- else:
1438- self._set_current_page(self.tc_browser_vbox)
1439-
1440- def on_tc_back_button_clicked(self, *args, **kwargs):
1441- """T & C 'back' button was clicked, return to the previous page."""
1442- self._set_current_page(self.enter_details_vbox)
1443-
1444- def on_tc_browser_notify_load_status(self, browser, *args, **kwargs):
1445- """The T&C page is being loaded."""
1446- from gi.repository import WebKit # pylint: disable=E0611
1447-
1448- if browser.get_load_status().real == WebKit.LoadStatus.FINISHED:
1449- self._set_current_page(self.tc_browser_vbox)
1450-
1451- def on_tc_browser_navigation_requested(self, browser, frame, request,
1452- action, decision, *args, **kwargs):
1453- """The user wants to navigate within the T&C browser."""
1454- from gi.repository import WebKit # pylint: disable=E0611
1455-
1456- if action is not None and \
1457- action.get_reason() == WebKit.WebNavigationReason.LINK_CLICKED:
1458- if decision is not None:
1459- decision.ignore()
1460- url = action.get_original_uri()
1461- webbrowser.open(url)
1462- else:
1463- if decision is not None:
1464- decision.use()
1465-
1466- def on_tc_browser_vbox_hide(self, *args, **kwargs):
1467- """The T&C page is no longer being shown."""
1468- children = self.tc_browser_window.get_children()
1469- if len(children) > 0:
1470- browser = children[0]
1471- self.tc_browser_window.remove(browser)
1472- browser.destroy()
1473- del browser
1474-
1475- def on_captcha_reload_button_clicked(self, *args, **kwargs):
1476- """User clicked the reload captcha button."""
1477- self._generate_captcha()
1478-
1479- # backend callbacks
1480-
1481- def _build_general_error_message(self, errordict):
1482- """Concatenate __all__ and message from the errordict."""
1483- result = None
1484- msg1 = errordict.get('__all__')
1485- msg2 = errordict.get('message')
1486- if msg1 is not None and msg2 is not None:
1487- result = '\n'.join((msg1, msg2))
1488- else:
1489- result = msg1 if msg1 is not None else msg2
1490- return result
1491-
1492- @log_call
1493- def on_captcha_generated(self, app_name, captcha_id, *args, **kwargs):
1494- """Captcha image has been generated and is available to be shown."""
1495- if captcha_id is None:
1496- logger.warning('on_captcha_generated: captcha_id is None for '
1497- 'app_name %r.', app_name)
1498- self._captcha_id = captcha_id
1499- self._set_captcha_image()
1500-
1501- @log_call
1502- def on_captcha_generation_error(self, app_name, error, *args, **kwargs):
1503- """Captcha image generation failed."""
1504- self._set_warning_message(self.warning_label, CAPTCHA_LOAD_ERROR)
1505- self._generate_captcha()
1506-
1507- @log_call
1508- def on_user_registered(self, app_name, email, *args, **kwargs):
1509- """Registration can go on, user needs to verify email."""
1510- help_text = VERIFY_EMAIL_LABEL % {'app_name': self.app_name,
1511- 'email': email}
1512- self.verify_email_vbox.help_text = help_text
1513- self._set_current_page(self.verify_email_vbox)
1514-
1515- @log_call
1516- def on_user_registration_error(self, app_name, error, *args, **kwargs):
1517- """Error in the data provided for registration."""
1518- msg = error.get('email')
1519- if msg is not None:
1520- self.email1_entry.set_warning(msg)
1521- self.email2_entry.set_warning(msg)
1522-
1523- msg = error.get('password')
1524- if msg is not None:
1525- self.password1_entry.set_warning(msg)
1526- self.password2_entry.set_warning(msg)
1527-
1528- msg = self._build_general_error_message(error)
1529- self._generate_captcha()
1530- self._set_current_page(self.enter_details_vbox, warning_text=msg)
1531-
1532- @log_call
1533- def on_email_validated(self, app_name, email, *args, **kwargs):
1534- """User email was successfully verified."""
1535- self.finish_success()
1536-
1537- @log_call
1538- def on_email_validation_error(self, app_name, error, *args, **kwargs):
1539- """User email validation failed."""
1540- msg = error.get('email_token')
1541- if msg is not None:
1542- self.email_token_entry.set_warning(msg)
1543-
1544- msg = self._build_general_error_message(error)
1545- self._set_current_page(self.verify_email_vbox, warning_text=msg)
1546-
1547- @log_call
1548- def on_logged_in(self, app_name, email, *args, **kwargs):
1549- """User was successfully logged in."""
1550- self.finish_success()
1551-
1552- @log_call
1553- def on_login_error(self, app_name, error, *args, **kwargs):
1554- """User was not successfully logged in."""
1555- msg = self._build_general_error_message(error)
1556- self._set_current_page(self.login_vbox, warning_text=msg)
1557-
1558- @log_call
1559- def on_user_not_validated(self, app_name, email, *args, **kwargs):
1560- """User was not validated."""
1561- self.on_user_registered(app_name, email)
1562-
1563- @log_call
1564- def on_password_reset_token_sent(self, app_name, email, *args, **kwargs):
1565- """Password reset token was successfully sent."""
1566- msg = SET_NEW_PASSWORD_LABEL % {'email': email}
1567- self.set_new_password_vbox.help_text = msg
1568- self._set_current_page(self.set_new_password_vbox)
1569-
1570- @log_call
1571- def on_password_reset_error(self, app_name, error, *args, **kwargs):
1572- """Password reset failed."""
1573- msg = self._build_general_error_message(error)
1574- self._set_current_page(self.login_vbox, warning_text=msg)
1575-
1576- @log_call
1577- def on_password_changed(self, app_name, email, *args, **kwargs):
1578- """Password was successfully changed."""
1579- self._set_current_page(self.login_vbox,
1580- warning_text=PASSWORD_CHANGED)
1581-
1582- @log_call
1583- def on_password_change_error(self, app_name, error, *args, **kwargs):
1584- """Password reset failed."""
1585- msg = self._build_general_error_message(error)
1586- self._set_current_page(self.request_password_token_vbox,
1587- warning_text=msg)
1588-
1589-
1590-def run(**kwargs):
1591- """Start the GTK mainloop and open the main window."""
1592- UbuntuSSOClientGUI(close_callback=Gtk.main_quit, **kwargs)
1593- Gtk.main()
1594
1595=== removed directory 'softwarecenter/sso/tests'
1596=== removed file 'softwarecenter/sso/tests/__init__.py'
1597--- softwarecenter/sso/tests/__init__.py 2012-11-28 17:08:44 +0000
1598+++ softwarecenter/sso/tests/__init__.py 1970-01-01 00:00:00 +0000
1599@@ -1,27 +0,0 @@
1600-# -*- coding: utf-8 -*-
1601-#
1602-# Copyright 2009-2012 Canonical Ltd.
1603-#
1604-# This program is free software: you can redistribute it and/or modify it
1605-# under the terms of the GNU General Public License version 3, as published
1606-# by the Free Software Foundation.
1607-#
1608-
1609-"""Tests for the Ubuntu SSO GTK+ graphical interface."""
1610-
1611-APP_NAME = u'I ♥ Ubuntu'
1612-CAPTCHA_ID = u'test ñiña'
1613-CAPTCHA_SOLUTION = u'william Byrd ñandú'
1614-EMAIL = u'test@example.com'
1615-EMAIL_TOKEN = u'B2P☺ gtf'
1616-HELP_TEXT = u'☛ Lorem ipsum dolor sit amet, consectetur adipiscing elit. ' \
1617- 'Nam sed lorem nibh. Suspendisse gravida nulla non nunc suscipit ' \
1618- 'pulvinar ' \
1619- 'tempus ut augue. Morbi consequat, ligula a elementum pretium, ' \
1620- 'dolor nulla tempus metus, sed viverra nisi risus non velit.'
1621-NAME = u'Juanito ☀ Pérez'
1622-PASSWORD = u'h3lloWorld☑ '
1623-PING_URL = u'http://localhost/ping-me/'
1624-RESET_PASSWORD_TOKEN = u'8G5Wtq'
1625-TC_URL = u'http://localhost/'
1626-UNKNOWN_ERROR = u'Something went very wrong! ☹'
1627
1628=== removed file 'softwarecenter/sso/tests/test_gui.py'
1629--- softwarecenter/sso/tests/test_gui.py 2012-11-23 22:57:21 +0000
1630+++ softwarecenter/sso/tests/test_gui.py 1970-01-01 00:00:00 +0000
1631@@ -1,2300 +0,0 @@
1632-# -*- coding: utf-8 -*-
1633-#
1634-# Copyright 2010-2012 Canonical Ltd.
1635-#
1636-# This program is free software: you can redistribute it and/or modify it
1637-# under the terms of the GNU General Public License version 3, as published
1638-# by the Free Software Foundation.
1639-#
1640-# This program is distributed in the hope that it will be useful, but
1641-# WITHOUT ANY WARRANTY; without even the implied warranties of
1642-# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
1643-# PURPOSE. See the GNU General Public License for more details.
1644-#
1645-# You should have received a copy of the GNU General Public License along
1646-# with this program. If not, see <http://www.gnu.org/licenses/>.
1647-
1648-"""Tests for the GUI for registration/login."""
1649-
1650-import itertools
1651-import logging
1652-import os
1653-
1654-from collections import defaultdict
1655-from functools import partial
1656-from unittest import skip, TestCase
1657-
1658-# pylint: disable=E0611
1659-from gi.repository import Gdk, Gtk, WebKit
1660-# pylint: enable=E0611
1661-from mock import patch
1662-
1663-from softwarecenter.sso import gui
1664-from softwarecenter.sso.tests import (
1665- APP_NAME,
1666- CAPTCHA_ID,
1667- CAPTCHA_SOLUTION,
1668- EMAIL,
1669- EMAIL_TOKEN,
1670- HELP_TEXT,
1671- NAME,
1672- PASSWORD,
1673- PING_URL,
1674- RESET_PASSWORD_TOKEN,
1675- TC_URL,
1676- UNKNOWN_ERROR,
1677-)
1678-
1679-# Access to a protected member 'yyy' of a client class
1680-# pylint: disable=W0212
1681-
1682-# Instance of 'UbuntuSSOClientGUI' has no 'yyy' member
1683-# pylint: disable=E1101,E1103
1684-
1685-# Use of super on an old style class
1686-# pylint: disable=E1002
1687-
1688-
1689-class FakedSSOBackend(object):
1690- """Fake a SSO Backend."""
1691-
1692- def __init__(self, *args, **kwargs):
1693- self._args = args
1694- self._kwargs = kwargs
1695- self._called = {}
1696- self.callbacks = defaultdict(list)
1697-
1698- for i in ('generate_captcha', 'login', 'login_and_ping',
1699- 'register_user', 'validate_email',
1700- 'validate_email_and_ping',
1701- 'request_password_reset_token',
1702- 'set_new_password'):
1703- setattr(self, i, self._record_call(i))
1704-
1705- def connect_to_signal(self, signal_name, callback):
1706- """Connect a callback to a given signal."""
1707- self.callbacks[signal_name].append(callback)
1708- return callback
1709-
1710- def disconnect_from_signal(self, signal_name, match):
1711- """Disconnect from a given signal."""
1712- self.callbacks[signal_name].remove(match)
1713- if len(self.callbacks[signal_name]) == 0:
1714- self.callbacks.pop(signal_name)
1715-
1716- def _record_call(self, func_name):
1717- """Store values when calling 'func_name'."""
1718-
1719- def inner(*args, **kwargs):
1720- """Fake 'func_name'."""
1721- self._called[func_name] = (args, kwargs)
1722-
1723- return inner
1724-
1725-
1726-class FakedLogger(object):
1727-
1728- def __init__(self):
1729- self.records = defaultdict(list)
1730- self.debug = partial(self.log, logging.DEBUG)
1731- self.info = partial(self.log, logging.INFO)
1732- self.warning = partial(self.log, logging.WARNING)
1733- self.error = self.exception = partial(self.log, logging.ERROR)
1734-
1735- def log(self, level, msg, *args):
1736- """Fake a logging operation."""
1737- self.records[level].append(msg % args)
1738-
1739- def check(self, level, *msgs):
1740- """Return whether there is a log entry for 'level' with 'msgs'."""
1741- result = all(any(msg in r for r in self.records[level])
1742- for msg in msgs)
1743- return result
1744-
1745- def reset(self):
1746- """Reset internal state."""
1747- self.records.clear()
1748-
1749-
1750-class Settings(dict):
1751- """Faked embedded browser settings."""
1752-
1753- def get_property(self, prop_name):
1754- """Alias for __getitem__."""
1755- return self[prop_name]
1756-
1757- def set_property(self, prop_name, newval):
1758- """Alias for __setitem__."""
1759- self[prop_name] = newval
1760-
1761-
1762-class FakedEmbeddedBrowser(Gtk.TextView):
1763- """Faked an embedded browser."""
1764-
1765- def __init__(self):
1766- super(FakedEmbeddedBrowser, self).__init__()
1767- self._props = {}
1768- self._signals = defaultdict(list)
1769- self._settings = Settings()
1770-
1771- def connect(self, signal_name, callback):
1772- """Connect 'signal_name' with 'callback'."""
1773- self._signals[signal_name].append(callback)
1774-
1775- def load_uri(self, uri):
1776- """Navigate to the given 'uri'."""
1777- self._props['uri'] = uri
1778-
1779- def set_property(self, prop_name, newval):
1780- """Set 'prop_name' to 'newval'."""
1781- self._props[prop_name] = newval
1782-
1783- def get_property(self, prop_name):
1784- """Return the current value for 'prop_name'."""
1785- return self._props[prop_name]
1786-
1787- def get_settings(self,):
1788- """Return the current settings."""
1789- return self._settings
1790-
1791- def get_load_status(self):
1792- """Return the current load status."""
1793- return WebKit.LoadStatus.FINISHED
1794-
1795- def show(self):
1796- """Show this instance."""
1797- self.set_property('visible', True)
1798-
1799-
1800-class BasicTestCase(TestCase):
1801- """Test case with a helper tracker."""
1802-
1803- def setUp(self):
1804- """Init."""
1805- super(BasicTestCase, self).setUp()
1806- self._called = False # helper
1807-
1808- self.memento = FakedLogger()
1809- self.patch(gui, 'logger', self.memento)
1810- self.patch(gui.sys, 'exit', lambda *a: None)
1811-
1812- def _set_called(self, *args, **kwargs):
1813- """Set _called to True."""
1814- self._called = (args, kwargs)
1815-
1816- def patch(self, obj, attr, new_value):
1817- patcher = patch.object(obj, attr, new_value)
1818- patcher.start()
1819- self.addCleanup(patcher.stop)
1820-
1821- def assert_color_equal(self, rgba_color, gdk_color):
1822- """Check that 'rgba_color' is the same as 'gdk_color'."""
1823- tmp = Gdk.RGBA()
1824- assert tmp.parse(gdk_color.to_string())
1825-
1826- msg = 'Text color must be %r (got %r instead).'
1827- self.assertEqual(rgba_color, tmp, msg % (rgba_color, tmp))
1828-
1829- def assert_backend_called(self, method, *args, **kwargs):
1830- """Check that 'method(*args, **kwargs)' was called in the backend."""
1831- self.assertIn(method, self.ui.backend._called)
1832-
1833- call = self.ui.backend._called[method]
1834- self.assertEqual(call[0], args)
1835-
1836- reply_handler = call[1].pop('reply_handler')
1837- self.assertEqual(reply_handler, gui.NO_OP)
1838-
1839- error_handler = call[1].pop('error_handler')
1840- self.assertEqual(error_handler.func, self.ui._handle_error)
1841-
1842- self.assertEqual(call[1], kwargs)
1843-
1844-
1845-class LabeledEntryTestCase(BasicTestCase):
1846- """Test suite for the labeled entry."""
1847-
1848- def setUp(self):
1849- """Init."""
1850- super(LabeledEntryTestCase, self).setUp()
1851- self.label = 'Test me please'
1852- self.entry = gui.LabeledEntry(label=self.label)
1853-
1854- # we need a window to be able to realize ourselves
1855- window = Gtk.Window()
1856- window.add(self.entry)
1857- window.show_all()
1858- self.addCleanup(window.hide)
1859- self.addCleanup(window.destroy)
1860-
1861- def grab_focus(self, focus_in=True):
1862- """Grab focus on widget, if None use self.entry."""
1863- direction = 'in' if focus_in else 'out'
1864- self.entry.emit('focus-%s-event' % direction, None)
1865-
1866- # Bad first argument 'LabeledEntry' given to super class
1867- # pylint: disable=E1003
1868- def assert_correct_label(self):
1869- """Check that the entry has the correct label."""
1870- # text content is correct
1871- msg = 'Text content must be %r (got %r instead).'
1872- expected = self.label
1873- actual = super(gui.LabeledEntry, self.entry).get_text()
1874- self.assertEqual(expected, actual, msg % (expected, actual))
1875-
1876- # text color is correct
1877- expected = gui.HELP_TEXT_COLOR
1878- actual = self.entry.get_style().text[Gtk.StateFlags.NORMAL]
1879- self.assert_color_equal(expected, actual)
1880-
1881- def test_initial_text(self):
1882- """Entry have the correct text at startup."""
1883- self.assert_correct_label()
1884-
1885- def test_width_chars(self):
1886- """Entry have the correct width."""
1887- self.assertEqual(self.entry.get_width_chars(), gui.DEFAULT_WIDTH)
1888-
1889- def test_tooltip(self):
1890- """Entry have the correct tooltip."""
1891- msg = 'Tooltip must be %r (got %r instead).'
1892- expected = self.label
1893- actual = self.entry.get_tooltip_text()
1894- # tooltip is correct
1895- self.assertEqual(expected, actual, msg % (expected, actual))
1896-
1897- def test_clear_entry_on_focus_in(self):
1898- """Entry are cleared when focused."""
1899- self.grab_focus()
1900-
1901- msg = 'Entry must be cleared on focus in.'
1902- self.assertEqual('', self.entry.get_text(), msg)
1903-
1904- def test_text_defaults_to_theme_color_when_focus_in(self):
1905- """Entry restore its text color when focused in."""
1906- self.patch(self.entry, 'override_color', self._set_called)
1907-
1908- self.grab_focus()
1909-
1910- self.assertEqual(((Gtk.StateFlags.NORMAL, None), {}), self._called,
1911- 'Entry text color must be restore on focus in.')
1912-
1913- def test_refill_entry_on_focus_out_if_no_input(self):
1914- """Entry is re-filled with label when focused out if no user input."""
1915-
1916- self.grab_focus() # grab focus
1917- self.grab_focus(focus_in=False) # loose focus
1918-
1919- # Entry must be re-filled on focus out
1920- self.assert_correct_label()
1921-
1922- def test_refill_entry_on_focus_out_if_empty_input(self):
1923- """Entry is re-filled with label when focused out if empty input."""
1924-
1925- self.grab_focus() # grab focus
1926-
1927- self.entry.set_text(' ') # add empty text to the entry
1928-
1929- self.grab_focus(focus_in=False) # loose focus
1930-
1931- # Entry must be re-filled on focus out
1932- self.assert_correct_label()
1933-
1934- def test_preserve_input_on_focus_out_if_user_input(self):
1935- """Entry is unmodified when focused out if user input."""
1936- msg = 'Entry must be left unmodified on focus out when user input.'
1937- expected = 'test me please'
1938-
1939- self.grab_focus() # grab focus
1940-
1941- self.entry.set_text(expected) # add empty text to the entry
1942-
1943- self.grab_focus(focus_in=False) # loose focus
1944-
1945- self.assertEqual(expected, self.entry.get_text(), msg)
1946-
1947- def test_preserve_input_on_focus_out_and_in_again(self):
1948- """Entry is unmodified when focused out and then in again."""
1949- msg = 'Entry must be left unmodified on focus out and then in again.'
1950- expected = 'test me I mean it'
1951-
1952- self.grab_focus() # grab focus
1953-
1954- self.entry.set_text(expected) # add text to the entry
1955-
1956- self.grab_focus(focus_in=False) # loose focus
1957- self.grab_focus() # grab focus again!
1958-
1959- self.assertEqual(expected, self.entry.get_text(), msg)
1960-
1961- def test_get_text_ignores_label(self):
1962- """Entry's text is only user input (label is ignored)."""
1963- self.assertEqual(self.entry.get_text(), '')
1964-
1965- def test_get_text_ignores_empty_input(self):
1966- """Entry's text is only user input (empty text is ignored)."""
1967- self.entry.set_text(' ')
1968- self.assertEqual(self.entry.get_text(), '')
1969-
1970- def test_get_text_doesnt_ignore_user_input(self):
1971- """Entry's text is user input."""
1972- self.entry.set_text('a')
1973- self.assertEqual(self.entry.get_text(), 'a')
1974-
1975- def test_no_warning_by_default(self):
1976- """No secondary icon by default."""
1977- self.assertEqual(self.entry.warning, None)
1978- self.assertEqual(self.entry.get_property('secondary-icon-stock'),
1979- None)
1980- self.assertEqual(self.entry.get_property('secondary-icon-sensitive'),
1981- False)
1982- self.assertEqual(self.entry.get_property('secondary-icon-activatable'),
1983- False)
1984- prop = self.entry.get_property('secondary-icon-tooltip-text')
1985- self.assertEqual(prop, None)
1986-
1987- def test_set_warning(self):
1988- """Setting a warning show the proper secondary icon."""
1989- msg = 'You failed!'
1990- self.entry.set_warning(msg)
1991- self.assertEqual(self.entry.warning, msg)
1992- self.assertEqual(self.entry.get_property('secondary-icon-stock'),
1993- Gtk.STOCK_DIALOG_WARNING)
1994- self.assertEqual(self.entry.get_property('secondary-icon-sensitive'),
1995- True)
1996- self.assertEqual(self.entry.get_property('secondary-icon-activatable'),
1997- False)
1998- prop = self.entry.get_property('secondary-icon-tooltip-text')
1999- self.assertEqual(prop, msg)
2000-
2001- def test_clear_warning(self):
2002- """Clearing a warning no longer show the secondary icon."""
2003- self.entry.clear_warning()
2004- self.assertEqual(self.entry.warning, None)
2005- self.assertEqual(self.entry.get_property('secondary-icon-stock'),
2006- None)
2007- self.assertEqual(self.entry.get_property('secondary-icon-sensitive'),
2008- False)
2009- self.assertEqual(self.entry.get_property('secondary-icon-activatable'),
2010- False)
2011- prop = self.entry.get_property('secondary-icon-tooltip-text')
2012- self.assertEqual(prop, None)
2013-
2014-
2015-class PasswordLabeledEntryTestCase(LabeledEntryTestCase):
2016- """Test suite for the labeled entry when is_password is True."""
2017-
2018- def setUp(self):
2019- """Init."""
2020- super(PasswordLabeledEntryTestCase, self).setUp()
2021- self.entry.is_password = True
2022-
2023- def test_password_fields_are_visible_at_startup(self):
2024- """Password entries show the helping text at startup."""
2025- self.assertTrue(self.entry.get_visibility(),
2026- 'Password entry should be visible at start up.')
2027-
2028- def test_password_field_is_visible_if_no_input_and_focus_out(self):
2029- """Password entry show the label when focus out."""
2030- self.grab_focus() # user clicked or TAB'd to the entry
2031- self.grab_focus(focus_in=False) # loose focus
2032- self.assertTrue(self.entry.get_visibility(),
2033- 'Entry should be visible when focus out and no input.')
2034-
2035- def test_password_fields_are_not_visible_when_editing(self):
2036- """Password entries show the hidden chars instead of the password."""
2037- self.grab_focus() # user clicked or TAB'd to the entry
2038- self.assertFalse(self.entry.get_visibility(),
2039- 'Entry should not be visible when editing.')
2040-
2041-
2042-class UbuntuSSOClientTestCase(BasicTestCase):
2043- """Basic setup and helper functions."""
2044-
2045- gui_class = gui.UbuntuSSOClientGUI
2046- kwargs = dict(app_name=APP_NAME, tc_url=TC_URL, help_text=HELP_TEXT)
2047-
2048- def setUp(self):
2049- """Init."""
2050- super(UbuntuSSOClientTestCase, self).setUp()
2051- self.patch(gui, 'get_sso_client', lambda: FakedSSOBackend())
2052- self.pages = ('enter_details', 'processing', 'verify_email', 'finish',
2053- 'tc_browser', 'login', 'request_password_token',
2054- 'set_new_password')
2055- self.ui = self.gui_class(**self.kwargs)
2056- self.addCleanup(self.ui.destroy)
2057- self.error = {'message': UNKNOWN_ERROR}
2058-
2059- def assert_entries_are_packed_to_ui(self, container_name, entries):
2060- """Every entry is properly packed in the ui 'container_name'."""
2061- msg = 'Entry %r must be packed in %r but is not.'
2062- container = getattr(self.ui, container_name)
2063- for kind in entries:
2064- name = '%s_entry' % kind
2065- entry = getattr(self.ui, name)
2066- self.assertIsInstance(entry, gui.LabeledEntry)
2067- self.assertIn(entry, container, msg % (name, container_name))
2068-
2069- def assert_warnings_visibility(self, visible=False):
2070- """Every warning label should be 'visible'."""
2071- msg = '%r should have %sempty content.'
2072- for name in self.ui.widgets:
2073- widget = getattr(self.ui, name)
2074- if 'warning' in name:
2075- self.assertEqual('', widget.get_text(),
2076- msg % (name, '' if visible else 'non-'))
2077- elif 'entry' in name:
2078- self.assertEqual(widget.warning, '')
2079-
2080- def assert_correct_label_warning(self, label, message):
2081- """Check that a warning is shown displaying 'message'."""
2082- # warning label is visible
2083- self.assertTrue(label.get_property('visible'))
2084-
2085- # warning content is correct
2086- actual = label.get_text().decode('utf-8')
2087- self.assertEqual(actual, message)
2088-
2089- # content color is correct
2090- # FIXME - New GTK+ 3.5 breaks this check - see bug #1014772
2091- # expected = gui.WARNING_TEXT_COLOR
2092- # actual = label.get_style().fg[Gtk.StateFlags.NORMAL]
2093- # self.assert_color_equal(expected, actual)
2094-
2095- def assert_correct_entry_warning(self, entry, message):
2096- """Check that a warning is shown displaying 'message'."""
2097- self.assertEqual(entry.warning, message)
2098-
2099- def assert_pages_visibility(self, **kwargs):
2100- """The page 'name' is the current page for the content notebook."""
2101- msg = 'page %r must be self.ui.content\'s current page.'
2102- (name, _), = kwargs.items()
2103- page = getattr(self.ui, '%s_vbox' % name)
2104- self.assertEqual(self.ui.content.get_current_page(),
2105- self.ui.content.page_num(page), msg % name)
2106-
2107- def click_join_with_valid_data(self):
2108- """Move to the next page after entering details."""
2109- self.ui.on_captcha_generated(app_name=APP_NAME, captcha_id=CAPTCHA_ID)
2110-
2111- self.ui.name_entry.set_text(NAME)
2112- # match emails
2113- self.ui.email1_entry.set_text(EMAIL)
2114- self.ui.email2_entry.set_text(EMAIL)
2115- # match passwords
2116- self.ui.password1_entry.set_text(PASSWORD)
2117- self.ui.password2_entry.set_text(PASSWORD)
2118- if self.ui.tc_url:
2119- # agree to TC, only if the tc_url is defined, so we catch errors
2120- self.ui.yes_to_tc_checkbutton.set_active(True)
2121- # resolve captcha properly
2122- self.ui.captcha_solution_entry.set_text(CAPTCHA_SOLUTION)
2123-
2124- self.ui.join_ok_button.clicked()
2125-
2126- def click_verify_email_with_valid_data(self):
2127- """Move to the next page after entering email token."""
2128- self.click_join_with_valid_data()
2129-
2130- # resolve email token properly
2131- self.ui.email_token_entry.set_text(EMAIL_TOKEN)
2132-
2133- self.ui.verify_token_button.clicked()
2134-
2135- def click_connect_with_valid_data(self):
2136- """Move to the next page after entering login info."""
2137- # enter email
2138- self.ui.login_email_entry.set_text(EMAIL)
2139- # enter password
2140- self.ui.login_password_entry.set_text(PASSWORD)
2141-
2142- self.ui.login_ok_button.clicked()
2143-
2144- def click_request_password_token_with_valid_data(self):
2145- """Move to the next page after requesting for password reset token."""
2146- # enter email
2147- self.ui.reset_email_entry.set_text(EMAIL)
2148-
2149- self.ui.request_password_token_ok_button.clicked()
2150-
2151- def click_set_new_password_with_valid_data(self):
2152- """Move to the next page after resetting password."""
2153- # enter reset code
2154- self.ui.reset_code_entry.set_text(RESET_PASSWORD_TOKEN)
2155- # match passwords
2156- self.ui.reset_password1_entry.set_text(PASSWORD)
2157- self.ui.reset_password2_entry.set_text(PASSWORD)
2158-
2159- self.ui.set_new_password_ok_button.clicked()
2160-
2161-
2162-class BasicUbuntuSSOClientTestCase(UbuntuSSOClientTestCase):
2163- """Test suite for basic functionality."""
2164-
2165- def test_main_window_is_visible_at_startup(self):
2166- """The main window is shown at startup."""
2167- self.assertTrue(self.ui.window.get_property('visible'))
2168-
2169- def test_main_window_is_resizable(self):
2170- """The main window can be resized."""
2171- self.assertTrue(self.ui.window.get_property('resizable'))
2172-
2173- def test_closing_main_window_calls_close_callback(self):
2174- """The close_callback is called when closing the main window."""
2175- self.ui.close_callback = self._set_called
2176- self.ui.on_close_clicked()
2177- self.assertTrue(self._called,
2178- 'close_callback was called when window was closed.')
2179-
2180- def test_close_callback_if_not_set(self):
2181- """The close_callback is a no op if not set."""
2182- self.ui.on_close_clicked()
2183- # no crash when close_callback is not set
2184-
2185- def test_app_name_is_stored(self):
2186- """The app_name is stored for further use."""
2187- self.assertIn(APP_NAME, self.ui.app_name)
2188-
2189- def test_signals_are_removed(self):
2190- """The hooked signals are removed at shutdown time."""
2191- assert len(self.ui.backend.callbacks) > 0 # at least one callback
2192-
2193- self.ui.on_close_clicked()
2194-
2195- self.assertEqual(self.ui.backend.callbacks, {})
2196-
2197- def test_pages_are_packed_into_container(self):
2198- """All the pages are packed in the main container."""
2199- children = self.ui.content.get_children()
2200- for page_name in self.pages:
2201- page = getattr(self.ui, '%s_vbox' % page_name)
2202- self.assertIn(page, children)
2203-
2204- def test_initial_text_for_entries(self):
2205- """Entries have the correct text at startup."""
2206- msg = 'Text for %r must be %r (got %r instead).'
2207- for name in self.ui.entries:
2208- entry = getattr(self.ui, name)
2209- expected = getattr(gui.ui_strings, name.upper())
2210- actual = entry.label
2211- # text content is correct
2212- self.assertEqual(expected, actual, msg % (name, expected, actual))
2213-
2214- def test_entries_activates_default(self):
2215- """Entries have the activates default prop set."""
2216- msg = '%r must have activates_default set to True.'
2217- for name in self.ui.entries:
2218- entry = getattr(self.ui, name)
2219- self.assertTrue(entry.get_activates_default(), msg % (name,))
2220-
2221- def test_password_fields_are_password(self):
2222- """Password fields have the is_password flag set."""
2223- msg = '%r should be a password LabeledEntry instance.'
2224- passwords = filter(lambda name: 'password' in name,
2225- self.ui.entries)
2226- for name in passwords:
2227- widget = getattr(self.ui, name)
2228- self.assertTrue(widget.is_password, msg % name)
2229-
2230- def test_warning_fields_are_cleared(self):
2231- """Every warning label should be cleared."""
2232- self.assert_warnings_visibility()
2233-
2234- def test_cancel_buttons_close_window(self):
2235- """Every cancel button should close the window when clicked."""
2236- self.patch(self.ui.backend, 'disconnect_from_signal', lambda *a: None)
2237- msg = '%r should close the window when clicked.'
2238- buttons = filter(lambda name: 'cancel_button' in name or
2239- 'close_button' in name, self.ui.widgets)
2240- for name in buttons:
2241- self.ui.close_callback = self._set_called
2242- widget = getattr(self.ui, name)
2243- widget.clicked()
2244- self.assertEqual(self._called, ((widget,), {}), msg % name)
2245- self._called = False
2246-
2247- def test_window_icon(self):
2248- """Main window has the proper icon."""
2249- self.assertEqual('ubuntu-logo', self.ui.window.get_icon_name())
2250-
2251- def test_finish_success_shows_success_page(self):
2252- """When calling 'finish_success' the success page is shown."""
2253- self.ui.finish_success()
2254- self.assert_pages_visibility(finish=True)
2255- self.assertEqual(gui.SUCCESS % {'app_name': APP_NAME},
2256- self.ui.finish_vbox.label.get_text().decode('utf8'))
2257- result = self.ui.finish_vbox.label.get_text().decode('utf8')
2258- self.assertTrue(self.ui.app_name in result)
2259-
2260- def test_finish_error_shows_error_page(self):
2261- """When calling 'finish_error' the error page is shown."""
2262- self.ui.finish_error()
2263- self.assert_pages_visibility(finish=True)
2264- self.assertEqual(gui.ERROR,
2265- self.ui.finish_vbox.label.get_text().decode('utf8'))
2266-
2267-
2268-@skip("Apparently, so far we can't use XLib dynamic bindings "
2269- "to complete the call to X11Window.foreign_new_for_display.")
2270-class SetTransientForTestCase(UbuntuSSOClientTestCase):
2271- """Test suite for setting the window as transient for another one."""
2272-
2273- def test_transient_window_is_none_if_window_id_is_zero(self):
2274- """The transient window is correct."""
2275- self.patch(gui.X11Window, 'foreign_new_for_display', self._set_called)
2276- ui = self.gui_class(window_id=0, **self.kwargs)
2277- self.addCleanup(ui.destroy)
2278-
2279- self.assertFalse(self._called, 'set_transient_for must not be called.')
2280-
2281- def test_transient_window_is_correct(self):
2282- """The transient window is correct."""
2283- xid = 5
2284- self.patch(gui.X11Window, 'foreign_new_for_display', self._set_called)
2285- ui = self.gui_class(window_id=xid, **self.kwargs)
2286- self.addCleanup(ui.destroy)
2287-
2288- self.assertTrue(self.memento.check(logging.ERROR, 'set_transient_for'))
2289- self.assertTrue(self.memento.check(logging.ERROR, str(xid)))
2290- self.assertEqual(self._called, ((xid,), {}))
2291-
2292- def test_transient_window_accepts_negative_id(self):
2293- """The transient window accepts a negative window id."""
2294- xid = -5
2295- self.patch(gui.X11Window, 'foreign_new_for_display', self._set_called)
2296- ui = self.gui_class(window_id=xid, **self.kwargs)
2297- self.addCleanup(ui.destroy)
2298-
2299- self.assertEqual(self._called, ((xid,), {}))
2300-
2301-
2302-class EnterDetailsTestCase(UbuntuSSOClientTestCase):
2303- """Test suite for the user registration (enter details page)."""
2304-
2305- def test_initial_text_for_header_label(self):
2306- """The header must have the correct text at startup."""
2307- msg = 'Text for the header must be %r (got %r instead).'
2308- expected = gui.JOIN_HEADER_LABEL % {'app_name': APP_NAME}
2309- actual = self.ui.header_label.get_text().decode('utf8')
2310- # text content is correct
2311- self.assertEqual(expected, actual, msg % (expected, actual))
2312-
2313- def test_entries_are_packed_to_ui(self):
2314- """Every entry is properly packed in the ui."""
2315- for kind in ('email', 'password'):
2316- container_name = '%ss_hbox' % kind
2317- entries = ('%s%s' % (kind, i) for i in xrange(1, 3))
2318- self.assert_entries_are_packed_to_ui(container_name, entries)
2319-
2320- self.assert_entries_are_packed_to_ui('enter_details_vbox', ('name',))
2321- self.assert_entries_are_packed_to_ui('captcha_solution_vbox',
2322- ('captcha_solution',))
2323- self.assert_entries_are_packed_to_ui('verify_email_details_vbox',
2324- ('email_token',))
2325-
2326- def test_initial_texts_for_checkbuttons(self):
2327- """Check buttons have the correct text at startup."""
2328- msg = 'Text for %r must be %r (got %r instead).'
2329- expected = gui.YES_TO_UPDATES % {'app_name': APP_NAME}
2330- actual = self.ui.yes_to_updates_checkbutton.get_label().decode('utf8')
2331- self.assertEqual(expected, actual, msg % ('yes_to_updates_checkbutton',
2332- expected, actual))
2333- expected = gui.YES_TO_TC % {'app_name': APP_NAME}
2334- actual = self.ui.yes_to_tc_checkbutton.get_label().decode('utf8')
2335- self.assertEqual(expected, actual,
2336- msg % ('yes_to_tc_checkbutton', expected, actual))
2337-
2338- def test_updates_checkbutton_is_checked_at_startup(self):
2339- """The 'yes to updates' checkbutton is checked by default."""
2340- msg = '%r is checked by default.'
2341- name = 'yes_to_updates_checkbutton'
2342- widget = getattr(self.ui, name)
2343- self.assertTrue(widget.get_active(), msg % name)
2344-
2345- def test_tc_checkbutton_is_not_checked_at_startup(self):
2346- """The 'yes to T&C' checkbutton is not checked by default."""
2347- msg = '%r is checked by default.'
2348- name = 'yes_to_tc_checkbutton'
2349- widget = getattr(self.ui, name)
2350- self.assertFalse(widget.get_active(), msg % name)
2351-
2352- def test_vboxes_visible_properties(self):
2353- """Only 'enter_details' vbox is visible at start up."""
2354- self.assert_pages_visibility(enter_details=True)
2355-
2356- def test_join_ok_button_clicked(self):
2357- """Clicking 'join_ok_button' sends info to backend using 'register'."""
2358- self.click_join_with_valid_data()
2359-
2360- # assert register_user was called
2361- self.assert_backend_called('register_user',
2362- APP_NAME, EMAIL, PASSWORD, NAME, CAPTCHA_ID, CAPTCHA_SOLUTION)
2363-
2364- def test_join_ok_button_clicked_morphs_to_processing_page(self):
2365- """Clicking 'join_ok_button' presents the processing vbox."""
2366- self.click_join_with_valid_data()
2367- self.assert_pages_visibility(processing=True)
2368-
2369- def test_processing_vbox_displays_an_active_spinner(self):
2370- """When processing the registration, an active spinner is shown."""
2371- self.click_join_with_valid_data()
2372-
2373- self.assertTrue(self.ui.processing_vbox.get_property('visible'),
2374- 'the processing box should be visible.')
2375-
2376- box = self.ui.processing_vbox.get_children()[0].get_children()[0]
2377- self.assertEqual(2, len(box.get_children()),
2378- 'processing_vbox must have two children.')
2379-
2380- spinner, label = box.get_children()
2381- self.assertIsInstance(spinner, Gtk.Spinner)
2382- self.assertIsInstance(label, Gtk.Label)
2383-
2384- self.assertTrue(spinner.get_property('visible'),
2385- 'the processing spinner should be visible.')
2386- self.assertTrue(spinner.get_property('active'),
2387- 'the processing spinner should be active.')
2388- self.assertTrue(label.get_property('visible'),
2389- 'the processing label should be visible.')
2390- self.assertEqual(label.get_text().decode('utf8'),
2391- gui.ONE_MOMENT_PLEASE,
2392- 'the processing label text must be correct.')
2393-
2394- def test_captcha_image_is_not_visible_at_startup(self):
2395- """Captcha image is not shown at startup."""
2396- self.assertFalse(self.ui.captcha_image.get_property('visible'),
2397- 'the captcha_image should not be visible.')
2398-
2399- def test_captcha_filename_is_different_each_time(self):
2400- """The captcha image is different each time."""
2401- ui = self.gui_class(**self.kwargs)
2402- self.addCleanup(ui.destroy)
2403-
2404- self.assertNotEqual(self.ui._captcha_filename, ui._captcha_filename)
2405-
2406- def test_captcha_image_is_removed_when_exiting(self):
2407- """The captcha image is removed at shutdown time."""
2408- open(self.ui._captcha_filename, 'w').close()
2409- assert os.path.exists(self.ui._captcha_filename)
2410- self.ui.on_close_clicked()
2411-
2412- self.assertFalse(os.path.exists(self.ui._captcha_filename),
2413- 'captcha image must be removed when exiting.')
2414-
2415- def test_captcha_image_is_a_spinner_at_first(self):
2416- """Captcha image shows a spinner until the image is downloaded."""
2417- self.assertTrue(self.ui.captcha_loading.get_property('visible'),
2418- 'the captcha_loading box should be visible.')
2419-
2420- box = self.ui.captcha_loading.get_children()[0].get_children()[0]
2421- self.assertEqual(2, len(box.get_children()),
2422- 'captcha_loading must have two children.')
2423-
2424- spinner, label = box.get_children()
2425- self.assertIsInstance(spinner, Gtk.Spinner)
2426- self.assertIsInstance(label, Gtk.Label)
2427-
2428- self.assertTrue(spinner.get_property('visible'),
2429- 'the captcha_loading spinner should be visible.')
2430- self.assertTrue(spinner.get_property('active'),
2431- 'the captcha_loading spinner should be active.')
2432- self.assertTrue(label.get_property('visible'),
2433- 'the captcha_loading label should be visible.')
2434- self.assertEqual(label.get_text().decode('utf8'), gui.LOADING,
2435- 'the captcha_loading label text must be correct.')
2436-
2437- def test_join_ok_button_is_disabled_until_captcha_is_available(self):
2438- """The join_ok_button is not sensitive until captcha is available."""
2439- self.assertFalse(self.ui.join_ok_button.is_sensitive())
2440-
2441- def test_join_ok_button_is_enabled_when_captcha_is_available(self):
2442- """The join_ok_button is sensitive when captcha is available."""
2443- self.ui.on_captcha_generated(app_name=APP_NAME, captcha_id=CAPTCHA_ID)
2444- self.assertTrue(self.ui.join_ok_button.is_sensitive())
2445-
2446- def test_captcha_loading_is_hid_when_captcha_is_available(self):
2447- """The captcha_loading is hid when captcha is available."""
2448- self.ui.on_captcha_generated(app_name=APP_NAME, captcha_id=CAPTCHA_ID)
2449- self.assertFalse(self.ui.captcha_loading.get_property('visible'),
2450- 'captcha_loading is not visible.')
2451-
2452- def test_captcha_id_is_stored_when_captcha_is_available(self):
2453- """The captcha_id is stored when captcha is available."""
2454- self.ui.on_captcha_generated(app_name=APP_NAME, captcha_id=CAPTCHA_ID)
2455- self.assertEqual(CAPTCHA_ID, self.ui._captcha_id)
2456-
2457- def test_captcha_image_is_requested_as_startup(self):
2458- """The captcha image is requested at startup."""
2459- # assert generate_captcha was called
2460- self.assert_backend_called('generate_captcha',
2461- APP_NAME, self.ui._captcha_filename)
2462-
2463- def test_captcha_is_shown_when_available(self):
2464- """The captcha image is shown when available."""
2465- self.patch(self.ui.captcha_image, 'set_from_file', self._set_called)
2466- self.ui.on_captcha_generated(app_name=APP_NAME, captcha_id=CAPTCHA_ID)
2467- self.assertTrue(self.ui.captcha_image.get_property('visible'))
2468- self.assertEqual(self._called, ((self.ui._captcha_filename,), {}))
2469-
2470- def test_on_captcha_generated_logs_captcha_id_when_none(self):
2471- """If the captcha id is None, a warning is logged."""
2472- self.ui.on_captcha_generated(app_name=APP_NAME, captcha_id=None)
2473- self.assertTrue(self.memento.check(logging.WARNING, repr(APP_NAME)))
2474- self.assertTrue(self.memento.check(logging.WARNING,
2475- 'captcha_id is None'))
2476-
2477- def test_captcha_reload_button_visible(self):
2478- """The captcha reload button is initially visible."""
2479- self.assertTrue(self.ui.captcha_reload_button.get_visible(),
2480- "The captcha button is not visible")
2481-
2482- def test_captcha_reload_button_reloads_captcha(self):
2483- """The captcha reload button loads a new captcha."""
2484- self.ui.on_captcha_generated(app_name=APP_NAME, captcha_id=CAPTCHA_ID)
2485- self.patch(self.ui, '_generate_captcha', self._set_called)
2486- self.ui.captcha_reload_button.clicked()
2487- self.assertEqual(self._called, ((), {}))
2488-
2489- def test_captcha_reload_button_has_tooltip(self):
2490- """The captcha reload button has a tooltip."""
2491- self.assertEqual(self.ui.captcha_reload_button.get_tooltip_text(),
2492- gui.CAPTCHA_RELOAD_TOOLTIP)
2493-
2494- def test_login_button_has_correct_wording(self):
2495- """The sign in button has the proper wording."""
2496- actual = self.ui.login_button.get_label().decode('utf8')
2497- self.assertEqual(gui.LOGIN_BUTTON_LABEL, actual)
2498-
2499- def test_join_ok_button_does_nothing_if_clicked_but_disabled(self):
2500- """The join form can only be submitted if the button is sensitive."""
2501- self.patch(self.ui.email1_entry, 'get_text', self._set_called)
2502-
2503- self.ui.join_ok_button.set_sensitive(False)
2504- self.ui.join_ok_button.clicked()
2505- self.assertFalse(self._called)
2506-
2507- self.ui.join_ok_button.set_sensitive(True)
2508- self.ui.join_ok_button.clicked()
2509- self.assertTrue(self._called)
2510-
2511- def test_user_and_pass_are_cached(self):
2512- """Username and password are temporarily cached for further use."""
2513- self.click_join_with_valid_data()
2514- self.assertEqual(self.ui.user_email, EMAIL)
2515- self.assertEqual(self.ui.user_password, PASSWORD)
2516-
2517- def test_on_captcha_generation_error(self):
2518- """on_captcha_generation_error shows an error and reloads captcha."""
2519- self.patch(self.ui, '_generate_captcha', self._set_called)
2520- self.ui.on_captcha_generation_error(APP_NAME, error=self.error)
2521- self.assert_correct_label_warning(self.ui.warning_label,
2522- gui.CAPTCHA_LOAD_ERROR)
2523- self.assertEqual(self._called, ((), {}))
2524-
2525- def test_captcha_success_after_error(self):
2526- """When captcha was retrieved after error, the warning is removed."""
2527- self.ui.on_captcha_generation_error(APP_NAME, error=self.error)
2528- self.ui.on_captcha_generated(app_name=APP_NAME, captcha_id=CAPTCHA_ID)
2529- self.assertEqual(self.ui.warning_label.get_text().decode('utf8'), '')
2530-
2531- def test_has_tc_link(self):
2532- """The T&C button and checkbox are shown if the link is provided"""
2533- self.assertEqual(self.ui.tc_button.get_visible(), True)
2534- self.assertEqual(self.ui.yes_to_tc_checkbutton.get_visible(), True)
2535-
2536-
2537-class NoTermsAndConditionsTestCase(EnterDetailsTestCase):
2538- """Test suite for the user registration (with no t&c link)."""
2539-
2540- kwargs = dict(app_name=APP_NAME, tc_url='', help_text=HELP_TEXT)
2541-
2542- def test_has_tc_link(self):
2543- """The T&C button and checkbox are not shown if no link is provided"""
2544- self.assertEqual(self.ui.tc_vbox.get_visible(), False)
2545-
2546-
2547-class TermsAndConditionsBrowserTestCase(UbuntuSSOClientTestCase):
2548- """Test suite for the terms & conditions browser."""
2549-
2550- def setUp(self):
2551- super(TermsAndConditionsBrowserTestCase, self).setUp()
2552- self.patch(WebKit, 'WebView', FakedEmbeddedBrowser)
2553- self.patch(self.ui, '_webkit_init_ssl', self._set_called)
2554-
2555- self.ui.tc_button.clicked()
2556- self.addCleanup(self.ui.tc_browser_vbox.hide)
2557-
2558- children = self.ui.tc_browser_window.get_children()
2559- assert len(children) == 1
2560- self.browser = children[0]
2561-
2562- def test_ssl_validation(self):
2563- """The browser is set to validate SSL."""
2564- self.assertEqual(self._called, ((), {}),
2565- '_webkit_init_ssl should be called when creating a '
2566- 'webkit browser.')
2567-
2568- def test_tc_browser_is_created_when_tc_page_is_shown(self):
2569- """The browser is created when the TC button is clicked."""
2570- self.ui.on_tc_browser_notify_load_status(self.browser)
2571-
2572- children = self.ui.tc_browser_window.get_children()
2573- self.assertEqual(1, len(children))
2574-
2575- def test_is_visible(self):
2576- """The browser is visible."""
2577- self.assertIsInstance(self.browser, FakedEmbeddedBrowser)
2578- self.assertTrue(self.browser.get_property('visible'))
2579-
2580- def test_settings(self):
2581- """The browser settings are correct."""
2582- settings = self.browser.get_settings()
2583- self.assertFalse(settings.get_property('enable-plugins'))
2584- self.assertFalse(settings.get_property('enable-default-context-menu'))
2585-
2586- def test_tc_browser_is_destroyed_when_tc_page_is_hid(self):
2587- """The browser is destroyed when the TC page is hid."""
2588- self.ui.on_tc_browser_notify_load_status(self.browser)
2589- self.patch(self.browser, 'destroy', self._set_called)
2590- self.ui.tc_browser_vbox.hide()
2591- self.assertEqual(self._called, ((), {}))
2592-
2593- def test_tc_browser_is_removed_when_tc_page_is_hid(self):
2594- """The browser is removed when the TC page is hid."""
2595- self.ui.on_tc_browser_notify_load_status(self.browser)
2596-
2597- self.ui.tc_browser_vbox.hide()
2598-
2599- children = self.ui.tc_browser_window.get_children()
2600- self.assertEqual(0, len(children))
2601-
2602- def test_tc_button_clicked_morphs_into_processing_page(self):
2603- """Clicking the T&C button morphs into processing page."""
2604- self.assert_pages_visibility(processing=True)
2605-
2606- def test_tc_back_clicked_returns_to_previous_page(self):
2607- """Terms & Conditions back button return to previous page."""
2608- self.ui.on_tc_browser_notify_load_status(self.browser)
2609- self.ui.tc_back_button.clicked()
2610- self.assert_pages_visibility(enter_details=True)
2611-
2612- def test_tc_button_has_the_proper_wording(self):
2613- """Terms & Conditions has the proper wording."""
2614- self.assertEqual(self.ui.tc_button.get_label().decode('utf8'),
2615- gui.TC_BUTTON)
2616-
2617- def test_tc_has_no_help_text(self):
2618- """The help text is removed."""
2619- self.ui.on_tc_browser_notify_load_status(self.browser)
2620- self.assertEqual('', self.ui.help_label.get_text().decode('utf8'))
2621-
2622- def test_tc_browser_opens_the_proper_url(self):
2623- """Terms & Conditions browser shows the proper uri."""
2624- self.assertEqual(self.browser.get_property('uri'), TC_URL)
2625-
2626- @skip('Connecting to notify::load-status makes U1 terms navigation fail.')
2627- def test_notify_load_status_connected(self):
2628- """The 'notify::load-status' signal is connected."""
2629- expected = [self.ui.on_tc_browser_notify_load_status]
2630- self.assertEqual(self.browser._signals['notify::load-status'],
2631- expected)
2632-
2633- def test_notify_load_finished_connected(self):
2634- """The 'load-finished' signal is connected."""
2635- expected = [self.ui.on_tc_browser_notify_load_status]
2636- self.assertEqual(self.browser._signals['notify::load-status'],
2637- expected)
2638-
2639- def test_tc_loaded_morphs_into_tc_browser_vbox(self):
2640- """When the Terms & Conditions is loaded, show the browser window."""
2641- self.ui.on_tc_browser_notify_load_status(self.browser)
2642- self.assert_pages_visibility(tc_browser=True)
2643-
2644- def test_navigation_requested_connected(self):
2645- """The 'navigation-policy-decision-requested' signal is connected."""
2646- actual = self.browser._signals['navigation-policy-decision-requested']
2647- expected = [self.ui.on_tc_browser_navigation_requested]
2648- self.assertEqual(actual, expected)
2649-
2650- def test_navigation_requested_succeeds_for_no_clicking(self):
2651- """The navigation request succeeds when user hasn't clicked a link."""
2652- action = WebKit.WebNavigationAction()
2653- action.set_reason(WebKit.WebNavigationReason.OTHER)
2654-
2655- decision = WebKit.WebPolicyDecision()
2656- decision.use = self._set_called
2657-
2658- kwargs = dict(browser=self.browser, frame=None, request=None,
2659- action=action, decision=decision)
2660- self.ui.on_tc_browser_navigation_requested(**kwargs)
2661- self.assertEqual(self._called, ((), {}))
2662-
2663- def test_navigation_requested_ignores_clicked_links(self):
2664- """The navigation request is ignored if a link was clicked."""
2665- action = WebKit.WebNavigationAction()
2666- action.set_reason(WebKit.WebNavigationReason.LINK_CLICKED)
2667-
2668- decision = WebKit.WebPolicyDecision()
2669- decision.ignore = self._set_called
2670-
2671- self.patch(gui.webbrowser, 'open', lambda *args, **kwargs: None)
2672-
2673- kwargs = dict(browser=self.browser, frame=None, request=None,
2674- action=action, decision=decision)
2675- self.ui.on_tc_browser_navigation_requested(**kwargs)
2676- self.assertEqual(self._called, ((), {}))
2677-
2678- def test_navigation_requested_ignores_for_none(self):
2679- """The navigation request is ignored the request if params are None."""
2680- kwargs = dict(browser=None, frame=None, request=None,
2681- action=None, decision=None)
2682- self.ui.on_tc_browser_navigation_requested(**kwargs)
2683-
2684- def test_navigation_requested_opens_links_when_clicked(self):
2685- """The navigation request is opened on user's default browser
2686-
2687- (If the user opened a link by clicking into it).
2688-
2689- """
2690- url = 'http://something.com/yadda'
2691- action = WebKit.WebNavigationAction()
2692- action.set_reason(WebKit.WebNavigationReason.LINK_CLICKED)
2693- action.set_original_uri(url)
2694-
2695- decision = WebKit.WebPolicyDecision()
2696- decision.ignore = gui.NO_OP
2697-
2698- self.patch(gui.webbrowser, 'open', self._set_called)
2699-
2700- kwargs = dict(browser=self.browser, frame=None, request=None,
2701- action=action, decision=decision)
2702- self.ui.on_tc_browser_navigation_requested(**kwargs)
2703- self.assertEqual(self._called, ((url,), {}))
2704-
2705- def test_on_tc_button_clicked_no_child(self):
2706- """Test the tc loading with no child."""
2707- called = []
2708-
2709- def fake_add_browser():
2710- """Fake add browser."""
2711- called.append('fake_add_browser')
2712-
2713- self.patch(self.ui, '_add_webkit_browser', fake_add_browser)
2714- self.patch(self.ui.tc_browser_window, 'get_child', lambda: None)
2715-
2716- self.ui.on_tc_button_clicked()
2717- self.assertIn('fake_add_browser', called)
2718-
2719- def test_on_tc_button_clicked_child(self):
2720- """Test the tc loading with child."""
2721- called = []
2722-
2723- def fake_add_browser(i_self):
2724- """Fake add browser."""
2725- called.append('fake_add_browser')
2726-
2727- self.patch(self.ui, '_add_webkit_browser', fake_add_browser)
2728-
2729- browser = WebKit.WebView()
2730- self.ui.tc_browser_window.add(browser)
2731- self.ui.on_tc_button_clicked()
2732- self.assertNotIn('fake_add_browser', called)
2733-
2734-
2735-class RegistrationErrorTestCase(UbuntuSSOClientTestCase):
2736- """Test suite for the user registration error handling."""
2737-
2738- def setUp(self):
2739- """Init."""
2740- super(RegistrationErrorTestCase, self).setUp()
2741- self.click_join_with_valid_data()
2742-
2743- def test_previous_page_is_shown(self):
2744- """On UserRegistrationError the previous page is shown."""
2745- self.ui.on_user_registration_error(app_name=APP_NAME, error=self.error)
2746- self.assert_pages_visibility(enter_details=True)
2747-
2748- def test_captcha_is_reloaded(self):
2749- """On UserRegistrationError the captcha is reloaded."""
2750- self.patch(self.ui, '_generate_captcha', self._set_called)
2751- self.ui.on_user_registration_error(app_name=APP_NAME, error=self.error)
2752- self.assertEqual(self._called, ((), {}))
2753-
2754- def test_warning_label_is_shown(self):
2755- """On UserRegistrationError the warning label is shown."""
2756- self.ui.on_user_registration_error(app_name=APP_NAME, error=self.error)
2757- self.assert_correct_label_warning(self.ui.warning_label,
2758- UNKNOWN_ERROR)
2759-
2760- def test_specific_errors_from_backend_are_shown(self):
2761- """Specific errors from backend are used."""
2762- error = {'errtype': 'RegistrationError',
2763- 'message': 'We\'re so doomed.',
2764- 'email': 'Enter a valid e-mail address.',
2765- 'password': 'I don\'t like your password.',
2766- '__all__': 'Wrong captcha solution.'}
2767-
2768- self.ui.on_user_registration_error(app_name=APP_NAME, error=error)
2769-
2770- expected = '\n'.join((error['__all__'], error['message']))
2771- self.assert_correct_label_warning(self.ui.warning_label, expected)
2772- self.assert_correct_entry_warning(self.ui.email1_entry,
2773- error['email'])
2774- self.assert_correct_entry_warning(self.ui.email2_entry,
2775- error['email'])
2776- self.assert_correct_entry_warning(self.ui.password1_entry,
2777- error['password'])
2778- self.assert_correct_entry_warning(self.ui.password2_entry,
2779- error['password'])
2780-
2781-
2782-class VerifyEmailTestCase(UbuntuSSOClientTestCase):
2783- """Test suite for the user registration (verify email page)."""
2784-
2785- method = 'validate_email'
2786- method_args = (APP_NAME, EMAIL, PASSWORD, EMAIL_TOKEN)
2787-
2788- def setUp(self):
2789- """Init."""
2790- super(VerifyEmailTestCase, self).setUp()
2791- self.ui.on_user_registered(app_name=APP_NAME, email=EMAIL)
2792-
2793- def test_registration_successful_shows_verify_email_vbox(self):
2794- """Receiving 'registration_successful' shows the verify email vbox."""
2795- self.ui.on_user_registered(app_name=APP_NAME, email=EMAIL)
2796- self.assert_pages_visibility(verify_email=True)
2797-
2798- def test_help_label_display_correct_wording(self):
2799- """The help_label display VERIFY_EMAIL_LABEL."""
2800- msg = 'help_label must read %r (got %r instead).'
2801- actual = self.ui.help_label.get_label().decode('utf8')
2802- expected = gui.VERIFY_EMAIL_LABEL % {'app_name': APP_NAME,
2803- 'email': EMAIL}
2804- self.assertEqual(expected, actual, msg % (expected, actual))
2805-
2806- def test_on_verify_token_button_clicked_calls_backend(self):
2807- """Verify token button triggers call to backend."""
2808- self.click_verify_email_with_valid_data()
2809- self.assert_backend_called(self.method, *self.method_args)
2810-
2811- def test_on_verify_token_button_clicked(self):
2812- """Verify token uses cached user_email and user_password."""
2813- self.ui.user_email = 'test@me.com'
2814- self.ui.user_password = 'yadda-yedda'
2815- method_args = list(self.method_args)
2816- method_args[1] = self.ui.user_email
2817- method_args[2] = self.ui.user_password
2818-
2819- # resolve email token properly
2820- self.ui.email_token_entry.set_text(EMAIL_TOKEN)
2821-
2822- self.ui.on_verify_token_button_clicked()
2823- self.assert_backend_called(self.method, *tuple(method_args))
2824-
2825- def test_on_verify_token_button_shows_processing_page(self):
2826- """Verify token button triggers call to backend."""
2827- self.click_verify_email_with_valid_data()
2828- self.assert_pages_visibility(processing=True)
2829-
2830- def test_no_warning_messages_if_valid_data(self):
2831- """No warning messages are shown if the data is valid."""
2832- # this will certainly NOT generate warnings
2833- self.click_verify_email_with_valid_data()
2834- self.assert_warnings_visibility()
2835-
2836- def test_on_email_validated_shows_finish_page(self):
2837- """On email validated the finish page is shown."""
2838- self.ui.on_email_validated(app_name=APP_NAME, email=EMAIL)
2839- self.assert_pages_visibility(finish=True)
2840-
2841- def test_on_email_validated_does_not_clear_the_help_text(self):
2842- """On email validated the help text is not removed."""
2843- self.ui.on_email_validated(app_name=APP_NAME, email=EMAIL)
2844- self.assertEqual(self.ui.verify_email_vbox.help_text,
2845- self.ui.help_label.get_label().decode('utf8'))
2846-
2847- def test_on_email_validation_error_verify_email_is_shown(self):
2848- """On email validation error, the verify_email page is shown."""
2849- self.ui.on_email_validation_error(app_name=APP_NAME, error=self.error)
2850- self.assert_pages_visibility(verify_email=True)
2851- self.assert_correct_label_warning(self.ui.warning_label,
2852- UNKNOWN_ERROR)
2853-
2854- def test_specific_errors_from_backend_are_shown(self):
2855- """Specific errors from backend are used."""
2856- error = {'errtype': 'EmailValidationError',
2857- 'message': 'We\'re so doomed.',
2858- 'email_token': 'Enter a valid e-mail address.',
2859- '__all__': 'We all are gonna die.'}
2860-
2861- self.ui.on_email_validation_error(app_name=APP_NAME, error=error)
2862-
2863- expected = '\n'.join((error['__all__'], error['message']))
2864- self.assert_correct_label_warning(self.ui.warning_label, expected)
2865- self.assert_correct_entry_warning(self.ui.email_token_entry,
2866- error['email_token'])
2867-
2868- def test_success_label_is_correct(self):
2869- """The success message is correct."""
2870- self.assertEqual(gui.SUCCESS % {'app_name': APP_NAME},
2871- self.ui.success_vbox.label.get_text().decode('utf8'))
2872- markup = self.ui.success_vbox.label.get_label().decode('utf8')
2873- self.assertTrue('<span size="x-large">' in markup)
2874- self.assertTrue(self.ui.app_name in markup)
2875-
2876- def test_error_label_is_correct(self):
2877- """The error message is correct."""
2878- self.assertEqual(gui.ERROR,
2879- self.ui.error_vbox.label.get_text().decode('utf8'))
2880- markup = self.ui.error_vbox.label.get_label().decode('utf8')
2881- self.assertTrue('<span size="x-large">' in markup)
2882-
2883- def test_on_finish_close_button_clicked_closes_window(self):
2884- """When done the window is closed."""
2885- self.ui.finish_close_button.clicked()
2886- self.assertFalse(self.ui.window.get_property('visible'))
2887-
2888- def test_verify_token_button_does_nothing_if_clicked_but_disabled(self):
2889- """The email token can only be submitted if the button is sensitive."""
2890- self.patch(self.ui.email_token_entry, 'get_text', self._set_called)
2891-
2892- self.ui.verify_token_button.set_sensitive(False)
2893- self.ui.verify_token_button.clicked()
2894- self.assertFalse(self._called)
2895-
2896- self.ui.verify_token_button.set_sensitive(True)
2897- self.ui.verify_token_button.clicked()
2898- self.assertTrue(self._called)
2899-
2900- def test_after_email_validated_finish_success(self):
2901- """After email_validated is called, finish_success is called."""
2902- self.patch(self.ui, 'finish_success', self._set_called)
2903-
2904- self.ui.on_email_validated(app_name=APP_NAME, email=EMAIL)
2905-
2906- self.assertEqual(self._called, ((), {}))
2907-
2908-
2909-class VerifyEmailWithPingTestCase(VerifyEmailTestCase):
2910- """Test suite for the user registration (verify email page)."""
2911-
2912- kwargs = dict(app_name=APP_NAME, tc_url=TC_URL, help_text=HELP_TEXT,
2913- ping_url=PING_URL)
2914- method = 'validate_email_and_ping'
2915- method_args = (APP_NAME, EMAIL, PASSWORD, EMAIL_TOKEN, PING_URL)
2916-
2917-
2918-class VerifyEmailValidationTestCase(UbuntuSSOClientTestCase):
2919- """Test suite for the user registration validation (verify email page)."""
2920-
2921- def setUp(self):
2922- """Init."""
2923- super(VerifyEmailValidationTestCase, self).setUp()
2924- self.ui.on_user_registered(app_name=APP_NAME, email=EMAIL)
2925-
2926- def test_warning_is_shown_if_empty_email_token(self):
2927- """A warning message is shown if email token is empty."""
2928- self.ui.email_token_entry.set_text('')
2929-
2930- self.ui.verify_token_button.clicked()
2931-
2932- self.assert_correct_entry_warning(self.ui.email_token_entry,
2933- gui.FIELD_REQUIRED)
2934- self.assertNotIn('validate_email', self.ui.backend._called)
2935-
2936- def test_no_warning_messages_if_valid_data(self):
2937- """No warning messages are shown if the data is valid."""
2938- # this will certainly NOT generate warnings
2939- self.click_verify_email_with_valid_data()
2940-
2941- self.assert_warnings_visibility()
2942-
2943- def test_no_warning_messages_if_valid_data_after_invalid_data(self):
2944- """No warnings if the data is valid (with prior invalid data)."""
2945- # this will certainly generate warnings
2946- self.ui.verify_token_button.clicked()
2947-
2948- # this will certainly NOT generate warnings
2949- self.click_verify_email_with_valid_data()
2950-
2951- self.assert_warnings_visibility()
2952-
2953-
2954-class VerifyEmailLoginOnlyTestCase(VerifyEmailTestCase):
2955- """Test suite for the user login (verify email page)."""
2956-
2957- kwargs = dict(app_name=APP_NAME, tc_url=TC_URL, help_text=HELP_TEXT,
2958- login_only=True)
2959-
2960-
2961-class VerifyEmailValidationLoginOnlyTestCase(VerifyEmailValidationTestCase):
2962- """Test suite for the user login validation (verify email page)."""
2963-
2964- kwargs = dict(app_name=APP_NAME, tc_url=TC_URL, help_text=HELP_TEXT,
2965- login_only=True)
2966-
2967-
2968-class RegistrationValidationTestCase(UbuntuSSOClientTestCase):
2969- """Test suite for the user registration validations."""
2970-
2971- def setUp(self):
2972- """Init."""
2973- super(RegistrationValidationTestCase, self).setUp()
2974- self.ui.join_ok_button.set_sensitive(True)
2975-
2976- def test_warning_is_shown_if_name_empty(self):
2977- """A warning message is shown if name is empty."""
2978- self.ui.name_entry.set_text('')
2979-
2980- self.ui.join_ok_button.clicked()
2981-
2982- self.assert_correct_entry_warning(self.ui.name_entry,
2983- gui.FIELD_REQUIRED)
2984- self.assertNotIn('register_user', self.ui.backend._called)
2985-
2986- def test_warning_is_shown_if_empty_email(self):
2987- """A warning message is shown if emails are empty."""
2988- self.ui.email1_entry.set_text('')
2989- self.ui.email2_entry.set_text('')
2990-
2991- self.ui.join_ok_button.clicked()
2992-
2993- self.assert_correct_entry_warning(self.ui.email1_entry,
2994- gui.FIELD_REQUIRED)
2995- self.assert_correct_entry_warning(self.ui.email2_entry,
2996- gui.FIELD_REQUIRED)
2997- self.assertNotIn('register_user', self.ui.backend._called)
2998-
2999- def test_warning_is_shown_if_email_mismatch(self):
3000- """A warning message is shown if emails doesn't match."""
3001- self.ui.email1_entry.set_text(EMAIL)
3002- self.ui.email2_entry.set_text(EMAIL * 2)
3003-
3004- self.ui.join_ok_button.clicked()
3005-
3006- self.assert_correct_entry_warning(self.ui.email1_entry,
3007- gui.EMAIL_MISMATCH)
3008- self.assert_correct_entry_warning(self.ui.email2_entry,
3009- gui.EMAIL_MISMATCH)
3010- self.assertNotIn('register_user', self.ui.backend._called)
3011-
3012- def test_warning_is_shown_if_invalid_email(self):
3013- """A warning message is shown if email is invalid."""
3014- self.ui.email1_entry.set_text('q')
3015- self.ui.email2_entry.set_text('q')
3016-
3017- self.ui.join_ok_button.clicked()
3018-
3019- self.assert_correct_entry_warning(self.ui.email1_entry,
3020- gui.EMAIL_INVALID)
3021- self.assert_correct_entry_warning(self.ui.email2_entry,
3022- gui.EMAIL_INVALID)
3023- self.assertNotIn('register_user', self.ui.backend._called)
3024-
3025- def test_password_help_is_always_shown(self):
3026- """Password help text is correctly displayed."""
3027- self.assertTrue(self.ui.password_help_label.get_property('visible'),
3028- 'password help text is visible.')
3029- self.assertEqual(self.ui.password_help_label.get_text().decode('utf8'),
3030- gui.PASSWORD_HELP)
3031- self.assertNotIn('register_user', self.ui.backend._called)
3032-
3033- def test_warning_is_shown_if_password_mismatch(self):
3034- """A warning message is shown if password doesn't match."""
3035- self.ui.password1_entry.set_text(PASSWORD)
3036- self.ui.password2_entry.set_text(PASSWORD * 2)
3037-
3038- self.ui.join_ok_button.clicked()
3039-
3040- self.assert_correct_entry_warning(self.ui.password1_entry,
3041- gui.PASSWORD_MISMATCH)
3042- self.assert_correct_entry_warning(self.ui.password2_entry,
3043- gui.PASSWORD_MISMATCH)
3044- self.assertNotIn('register_user', self.ui.backend._called)
3045-
3046- def test_warning_is_shown_if_password_too_weak(self):
3047- """A warning message is shown if password is too weak."""
3048- # password will match but will be too weak
3049- for pwd in ('', 'h3lloWo', PASSWORD.lower(), 'helloWorld'):
3050- self.ui.password1_entry.set_text(pwd)
3051- self.ui.password2_entry.set_text(pwd)
3052-
3053- self.ui.join_ok_button.clicked()
3054-
3055- self.assert_correct_entry_warning(self.ui.password1_entry,
3056- gui.PASSWORD_TOO_WEAK)
3057- self.assert_correct_entry_warning(self.ui.password2_entry,
3058- gui.PASSWORD_TOO_WEAK)
3059- self.assertNotIn('register_user', self.ui.backend._called)
3060-
3061- def test_warning_is_shown_if_tc_not_accepted(self):
3062- """A warning message is shown if TC are not accepted."""
3063- # don't agree to TC
3064- self.ui.yes_to_tc_checkbutton.set_active(False)
3065-
3066- self.ui.join_ok_button.clicked()
3067-
3068- self.assert_correct_label_warning(self.ui.tc_warning_label,
3069- gui.TC_NOT_ACCEPTED % {'app_name': APP_NAME})
3070- self.assertNotIn('register_user', self.ui.backend._called)
3071-
3072- def test_warning_is_shown_if_not_captcha_solution(self):
3073- """A warning message is shown if TC are not accepted."""
3074- # captcha solution will be empty
3075- self.ui.captcha_solution_entry.set_text('')
3076-
3077- self.ui.join_ok_button.clicked()
3078-
3079- self.assert_correct_entry_warning(self.ui.captcha_solution_entry,
3080- gui.FIELD_REQUIRED)
3081- self.assertNotIn('register_user', self.ui.backend._called)
3082-
3083- def test_no_warning_messages_if_valid_data(self):
3084- """No warning messages are shown if the data is valid."""
3085- # this will certainly NOT generate warnings
3086- self.click_join_with_valid_data()
3087-
3088- self.assert_warnings_visibility()
3089-
3090- def test_no_warning_messages_if_valid_data_after_invalid_data(self):
3091- """No warnings if the data is valid (with prior invalid data)."""
3092- # this will certainly generate warnings
3093- self.ui.join_ok_button.clicked()
3094-
3095- # this will certainly NOT generate warnings
3096- self.click_join_with_valid_data()
3097-
3098- self.assert_warnings_visibility()
3099-
3100-
3101-class LoginTestCase(UbuntuSSOClientTestCase):
3102- """Test suite for the user login pages."""
3103-
3104- method = 'login'
3105- method_args = (APP_NAME, EMAIL, PASSWORD)
3106-
3107- def setUp(self):
3108- """Init."""
3109- super(LoginTestCase, self).setUp()
3110- self.ui.login_button.clicked()
3111-
3112- def test_login_button_clicked_morphs_to_login_page(self):
3113- """Clicking sig_in_button morphs window into login page."""
3114- self.assert_pages_visibility(login=True)
3115-
3116- def test_initial_text_for_header_label(self):
3117- """The header must have the correct text when logging in."""
3118- msg = 'Text for the header must be %r (got %r instead).'
3119- expected = gui.LOGIN_HEADER_LABEL % {'app_name': APP_NAME}
3120- actual = self.ui.header_label.get_text().decode('utf8')
3121- self.assertEqual(expected, actual, msg % (expected, actual))
3122-
3123- def test_initial_text_for_help_label(self):
3124- """The help must have the correct text at startup."""
3125- msg = 'Text for the help must be %r (got %r instead).'
3126- expected = gui.CONNECT_HELP_LABEL % {'app_name': APP_NAME}
3127- actual = self.ui.help_label.get_text().decode('utf8')
3128- self.assertEqual(expected, actual, msg % (expected, actual))
3129-
3130- def test_entries_are_packed_to_ui_for_login(self):
3131- """Every entry is properly packed in the ui for the login page."""
3132- entries = ('login_email', 'login_password')
3133- self.assert_entries_are_packed_to_ui('login_details_vbox', entries)
3134-
3135- def test_entries_are_packed_to_ui_for_set_new_password(self):
3136- """Every entry is packed in the ui for the reset password page."""
3137- entries = ('reset_code', 'reset_password1', 'reset_password2')
3138- self.assert_entries_are_packed_to_ui('set_new_password_details_vbox',
3139- entries)
3140-
3141- def test_entries_are_packed_to_ui_for_request_password_token(self):
3142- """Every entry is packed in the ui for the reset email page."""
3143- container_name = 'request_password_token_details_vbox'
3144- entries = ('reset_email',)
3145- self.assert_entries_are_packed_to_ui(container_name, entries)
3146-
3147- def test_on_login_back_button_clicked(self):
3148- """Clicking login_back_button show registration page."""
3149- self.ui.login_back_button.clicked()
3150- self.assert_pages_visibility(enter_details=True)
3151-
3152- def test_on_login_connect_button_clicked(self):
3153- """Clicking login_ok_button calls backend.login."""
3154- self.click_connect_with_valid_data()
3155- self.assert_backend_called(self.method, *self.method_args)
3156-
3157- def test_on_login_connect_button_clicked_morphs_to_processing_page(self):
3158- """Clicking login_ok_button morphs to the processing page."""
3159- self.click_connect_with_valid_data()
3160- self.assert_pages_visibility(processing=True)
3161-
3162- def test_on_logged_in_morphs_to_finish_page(self):
3163- """When user logged in the finish page is shown."""
3164- self.click_connect_with_valid_data()
3165- self.ui.on_logged_in(app_name=APP_NAME, email=EMAIL)
3166- self.assert_pages_visibility(finish=True)
3167-
3168- def test_on_login_error_morphs_to_login_page(self):
3169- """On user login error, the previous page is shown."""
3170- self.click_connect_with_valid_data()
3171- self.ui.on_login_error(app_name=APP_NAME, error=self.error)
3172- self.assert_pages_visibility(login=True)
3173-
3174- def test_on_user_not_validated_morphs_to_verify_page(self):
3175- """On user not validated, the verify page is shown."""
3176- self.click_connect_with_valid_data()
3177- self.ui.on_user_not_validated(app_name=APP_NAME, email=EMAIL)
3178- self.assert_pages_visibility(verify_email=True)
3179-
3180- def test_on_login_error_a_warning_is_shown(self):
3181- """On user login error, a warning is shown with proper wording."""
3182- self.click_connect_with_valid_data()
3183- self.ui.on_login_error(app_name=APP_NAME, error=self.error)
3184- self.assert_correct_label_warning(self.ui.warning_label,
3185- UNKNOWN_ERROR)
3186-
3187- def test_specific_errors_from_backend_are_shown(self):
3188- """Specific errors from backend are used."""
3189- error = {'errtype': 'AuthenticationError',
3190- 'message': 'We\'re so doomed.',
3191- '__all__': 'We all are gonna die.'}
3192-
3193- self.ui.on_login_error(app_name=APP_NAME, error=error)
3194-
3195- expected = '\n'.join((error['__all__'], error['message']))
3196- self.assert_correct_label_warning(self.ui.warning_label, expected)
3197-
3198- def test_back_to_registration_hides_warning(self):
3199- """After user login error, warning is hidden when clicking 'Back'."""
3200- self.click_connect_with_valid_data()
3201- self.ui.on_login_error(app_name=APP_NAME, error=self.error)
3202- self.ui.login_back_button.clicked()
3203- self.assert_warnings_visibility()
3204-
3205- def test_login_ok_button_does_nothing_if_clicked_but_disabled(self):
3206- """The join form can only be submitted if the button is sensitive."""
3207- self.patch(self.ui.login_email_entry, 'get_text', self._set_called)
3208-
3209- self.ui.login_ok_button.set_sensitive(False)
3210- self.ui.login_ok_button.clicked()
3211- self.assertFalse(self._called)
3212-
3213- self.ui.login_ok_button.set_sensitive(True)
3214- self.ui.login_ok_button.clicked()
3215- self.assertTrue(self._called)
3216-
3217- def test_user_and_pass_are_cached(self):
3218- """Username and password are temporarily cached for further use."""
3219- self.click_connect_with_valid_data()
3220- self.assertEqual(self.ui.user_email, EMAIL)
3221- self.assertEqual(self.ui.user_password, PASSWORD)
3222-
3223- def test_after_login_success_finish_success(self):
3224- """After logged_in is called, finish_success is called."""
3225- self.patch(self.ui, 'finish_success', self._set_called)
3226-
3227- self.ui.on_logged_in(app_name=APP_NAME, email=EMAIL)
3228-
3229- self.assertEqual(self._called, ((), {}))
3230-
3231-
3232-class LoginWithPingTestCase(LoginTestCase):
3233- """Test suite for the login when the ping_url is set."""
3234-
3235- kwargs = dict(app_name=APP_NAME, tc_url=TC_URL, help_text=HELP_TEXT,
3236- ping_url=PING_URL)
3237- method = 'login_and_ping'
3238- method_args = (APP_NAME, EMAIL, PASSWORD, PING_URL)
3239-
3240-
3241-class LoginValidationTestCase(UbuntuSSOClientTestCase):
3242- """Test suite for the user login validation."""
3243-
3244- def setUp(self):
3245- """Init."""
3246- super(LoginValidationTestCase, self).setUp()
3247- self.ui.login_button.clicked()
3248-
3249- def test_warning_is_shown_if_empty_email(self):
3250- """A warning message is shown if email is empty."""
3251- self.ui.login_email_entry.set_text('')
3252-
3253- self.ui.login_ok_button.clicked()
3254-
3255- self.assert_correct_entry_warning(self.ui.login_email_entry,
3256- gui.FIELD_REQUIRED)
3257- self.assertNotIn('login', self.ui.backend._called)
3258-
3259- def test_warning_is_shown_if_invalid_email(self):
3260- """A warning message is shown if email is invalid."""
3261- self.ui.login_email_entry.set_text('q')
3262-
3263- self.ui.login_ok_button.clicked()
3264-
3265- self.assert_correct_entry_warning(self.ui.login_email_entry,
3266- gui.EMAIL_INVALID)
3267- self.assertNotIn('login', self.ui.backend._called)
3268-
3269- def test_warning_is_shown_if_empty_password(self):
3270- """A warning message is shown if password is empty."""
3271- self.ui.login_password_entry.set_text('')
3272-
3273- self.ui.login_ok_button.clicked()
3274-
3275- self.assert_correct_entry_warning(self.ui.login_password_entry,
3276- gui.FIELD_REQUIRED)
3277- self.assertNotIn('login', self.ui.backend._called)
3278-
3279- def test_no_warning_messages_if_valid_data(self):
3280- """No warning messages are shown if the data is valid."""
3281- # this will certainly NOT generate warnings
3282- self.click_connect_with_valid_data()
3283-
3284- self.assert_warnings_visibility()
3285-
3286- def test_no_warning_messages_if_valid_data_after_invalid_data(self):
3287- """No warnings if the data is valid (with prior invalid data)."""
3288- # this will certainly generate warnings
3289- self.ui.login_ok_button.clicked()
3290-
3291- # this will certainly NOT generate warnings
3292- self.click_connect_with_valid_data()
3293-
3294- self.assert_warnings_visibility()
3295-
3296-
3297-class ResetPasswordTestCase(UbuntuSSOClientTestCase):
3298- """Test suite for the reset password functionality."""
3299-
3300- def setUp(self):
3301- """Init."""
3302- super(ResetPasswordTestCase, self).setUp()
3303- self.ui.login_button.clicked()
3304- self.ui.forgotten_password_button.clicked()
3305-
3306- def test_forgotten_password_button_has_the_proper_wording(self):
3307- """The forgotten_password_button has the proper wording."""
3308- actual = self.ui.forgotten_password_button.get_label()
3309- self.assertEqual(actual.decode('utf8'), gui.FORGOTTEN_PASSWORD_BUTTON)
3310-
3311- def test_on_forgotten_password_button_clicked_help_text(self):
3312- """Clicking forgotten_password_button the help is properly changed."""
3313- wanted = gui.REQUEST_PASSWORD_TOKEN_LABEL % {'app_name': APP_NAME}
3314- self.assertEqual(self.ui.help_label.get_text().decode('utf8'), wanted)
3315-
3316- def test_on_forgotten_password_button_clicked_header_label(self):
3317- """Clicking forgotten_password_button the title is properly changed."""
3318- self.assertEqual(self.ui.header_label.get_text().decode('utf8'),
3319- gui.RESET_PASSWORD)
3320-
3321- def test_on_forgotten_password_button_clicked_ok_button(self):
3322- """Clicking forgotten_password_button the ok button reads 'Next'."""
3323- actual = self.ui.request_password_token_ok_button.get_label()
3324- self.assertEqual(actual.decode('utf8'), gui.NEXT)
3325-
3326- def test_on_forgotten_password_button_clicked_morphs_window(self):
3327- """Clicking forgotten_password_button the proper page is shown."""
3328- self.assert_pages_visibility(request_password_token=True)
3329-
3330- def test_on_request_password_token_back_button_clicked(self):
3331- """Clicking request_password_token_back_button show login screen."""
3332- self.ui.request_password_token_back_button.clicked()
3333- self.assert_pages_visibility(login=True)
3334-
3335- def test_request_password_token_ok_button_disabled_until_email_added(self):
3336- """The button is disabled until email added."""
3337- is_sensitive = self.ui.request_password_token_ok_button.get_sensitive
3338- self.assertFalse(is_sensitive())
3339-
3340- self.ui.reset_email_entry.set_text('a')
3341- self.assertTrue(is_sensitive())
3342-
3343- self.ui.reset_email_entry.set_text('')
3344- self.assertFalse(is_sensitive())
3345-
3346- self.ui.reset_email_entry.set_text(' ')
3347- self.assertFalse(is_sensitive())
3348-
3349- def test_on_request_password_token_ok_button_clicked_morphs_window(self):
3350- """Clicking request_password_token_ok_button morphs processing page."""
3351- self.click_request_password_token_with_valid_data()
3352- self.assert_pages_visibility(processing=True)
3353-
3354- def test_on_request_password_token_ok_button_clicked_calls_backend(self):
3355- """Clicking request_password_token_ok_button the backend is called."""
3356- self.click_request_password_token_with_valid_data()
3357- self.assert_backend_called('request_password_reset_token',
3358- APP_NAME, EMAIL)
3359-
3360- def test_on_password_reset_token_sent_morphs_window(self):
3361- """When the reset token was sent, the reset password page is shown."""
3362- self.click_request_password_token_with_valid_data()
3363- self.ui.on_password_reset_token_sent(app_name=APP_NAME, email=EMAIL)
3364- self.assert_pages_visibility(set_new_password=True)
3365-
3366- def test_on_password_reset_token_sent_help_text(self):
3367- """Clicking request_password_token_ok_button changes the help text."""
3368- self.click_request_password_token_with_valid_data()
3369- self.ui.on_password_reset_token_sent(app_name=APP_NAME, email=EMAIL)
3370-
3371- self.assertEqual(self.ui.help_label.get_text().decode('utf8'),
3372- gui.SET_NEW_PASSWORD_LABEL % {'email': EMAIL})
3373-
3374- def test_on_password_reset_token_sent_ok_button(self):
3375- """After request_password_token_ok_button the ok button is updated."""
3376- self.click_request_password_token_with_valid_data()
3377- self.ui.on_password_reset_token_sent(app_name=APP_NAME, email=EMAIL)
3378-
3379- actual = self.ui.set_new_password_ok_button.get_label()
3380- self.assertEqual(actual.decode('utf8'), gui.RESET_PASSWORD)
3381-
3382- def test_on_password_reset_error_shows_login_page(self):
3383- """When reset token wasn't successfully sent, login page is shown."""
3384- self.ui.on_password_reset_error(app_name=APP_NAME, error=self.error)
3385- self.assert_correct_label_warning(self.ui.warning_label,
3386- UNKNOWN_ERROR)
3387- self.assert_pages_visibility(login=True)
3388-
3389- def test_specific_errors_from_backend_are_shown(self):
3390- """Specific errors from backend are used."""
3391- error = {'errtype': 'ResetPasswordTokenError',
3392- 'message': 'We\'re so doomed.',
3393- '__all__': 'We all are gonna die.'}
3394-
3395- self.ui.on_password_reset_error(app_name=APP_NAME, error=error)
3396-
3397- expected = '\n'.join((error['__all__'], error['message']))
3398- self.assert_correct_label_warning(self.ui.warning_label, expected)
3399-
3400- def test_ok_button_does_nothing_if_clicked_but_disabled(self):
3401- """The password token can be requested if the button is sensitive."""
3402- self.patch(self.ui.reset_email_entry, 'get_text', self._set_called)
3403-
3404- self.ui.request_password_token_ok_button.set_sensitive(False)
3405- self.ui.request_password_token_ok_button.clicked()
3406- self.assertFalse(self._called)
3407-
3408- self.ui.request_password_token_ok_button.set_sensitive(True)
3409- self.ui.request_password_token_ok_button.clicked()
3410- self.assertTrue(self._called)
3411-
3412-
3413-class ResetPasswordValidationTestCase(UbuntuSSOClientTestCase):
3414- """Test suite for the password reset validations."""
3415-
3416- def test_warning_is_shown_if_empty_email(self):
3417- """A warning message is shown if emails are empty."""
3418- self.ui.reset_email_entry.set_text(' ')
3419-
3420- self.ui.request_password_token_ok_button.set_sensitive(True)
3421- self.ui.request_password_token_ok_button.clicked()
3422-
3423- self.assert_correct_entry_warning(self.ui.reset_email_entry,
3424- gui.FIELD_REQUIRED)
3425- self.assertNotIn('request_password_reset_token',
3426- self.ui.backend._called)
3427-
3428- def test_warning_is_shown_if_invalid_email(self):
3429- """A warning message is shown if email is invalid."""
3430- self.ui.reset_email_entry.set_text('q')
3431-
3432- self.ui.request_password_token_ok_button.clicked()
3433-
3434- self.assert_correct_entry_warning(self.ui.reset_email_entry,
3435- gui.EMAIL_INVALID)
3436- self.assertNotIn('request_password_reset_token',
3437- self.ui.backend._called)
3438-
3439- def test_no_warning_messages_if_valid_data(self):
3440- """No warning messages are shown if the data is valid."""
3441- # this will certainly NOT generate warnings
3442- self.click_request_password_token_with_valid_data()
3443-
3444- self.assert_warnings_visibility()
3445-
3446- def test_no_warning_messages_if_valid_data_after_invalid_data(self):
3447- """No warnings if the data is valid (with prior invalid data)."""
3448- # this will certainly generate warnings
3449- self.ui.request_password_token_ok_button.clicked()
3450-
3451- # this will certainly NOT generate warnings
3452- self.click_request_password_token_with_valid_data()
3453-
3454- self.assert_warnings_visibility()
3455-
3456-
3457-class SetNewPasswordTestCase(UbuntuSSOClientTestCase):
3458- """Test suite for setting a new password functionality."""
3459-
3460- def setUp(self):
3461- """Init."""
3462- super(SetNewPasswordTestCase, self).setUp()
3463- self.click_request_password_token_with_valid_data()
3464- self.ui.on_password_reset_token_sent(app_name=APP_NAME, email=EMAIL)
3465-
3466- def test_on_set_new_password_ok_button_disabled(self):
3467- """The set_new_password_ok_button is disabled until values added."""
3468- self.click_request_password_token_with_valid_data()
3469- self.assertFalse(self.ui.set_new_password_ok_button.get_sensitive())
3470-
3471- msg = 'set_new_password_ok_button must be sensitive (%s) for %r.'
3472- entries = (self.ui.reset_code_entry,
3473- self.ui.reset_password1_entry,
3474- self.ui.reset_password2_entry)
3475- for values in itertools.product(('', ' ', 'a'), repeat=3):
3476- expected = True
3477- for entry, val in zip(entries, values):
3478- entry.set_text(val)
3479- expected &= bool(val and not val.isspace())
3480-
3481- actual = self.ui.set_new_password_ok_button.get_sensitive()
3482- self.assertEqual(expected, actual, msg % (expected, values))
3483-
3484- def test_on_set_new_password_ok_button_clicked_morphs_window(self):
3485- """Clicking set_new_password_ok_button the processing page is shown."""
3486- self.click_set_new_password_with_valid_data()
3487- self.assert_pages_visibility(processing=True)
3488-
3489- def test_on_set_new_password_ok_button_clicked_calls_backend(self):
3490- """Clicking set_new_password_ok_button the backend is called."""
3491- self.click_set_new_password_with_valid_data()
3492- self.assert_backend_called('set_new_password',
3493- APP_NAME, EMAIL, RESET_PASSWORD_TOKEN, PASSWORD)
3494-
3495- def test_on_password_changed_shows_login_page(self):
3496- """When password was successfully changed the login page is shown."""
3497- self.ui.on_password_changed(app_name=APP_NAME, email=EMAIL)
3498- self.assert_correct_label_warning(self.ui.warning_label,
3499- gui.PASSWORD_CHANGED)
3500- self.assert_pages_visibility(login=True)
3501-
3502- def test_on_password_change_error_shows_login_page(self):
3503- """When password wasn't changed the reset password page is shown."""
3504- self.ui.on_password_change_error(app_name=APP_NAME, error=self.error)
3505- self.assert_correct_label_warning(self.ui.warning_label,
3506- UNKNOWN_ERROR)
3507- self.assert_pages_visibility(request_password_token=True)
3508-
3509- def test_specific_errors_from_backend_are_shown(self):
3510- """Specific errors from backend are used."""
3511- error = {'errtype': 'NewPasswordError',
3512- 'message': 'We\'re so doomed.',
3513- '__all__': 'We all are gonna die.'}
3514-
3515- self.ui.on_password_change_error(app_name=APP_NAME, error=error)
3516-
3517- expected = '\n'.join((error['__all__'], error['message']))
3518- self.assert_correct_label_warning(self.ui.warning_label, expected)
3519-
3520- def test_ok_button_does_nothing_if_clicked_but_disabled(self):
3521- """The new password can only be set if the button is sensitive."""
3522- self.patch(self.ui.reset_code_entry, 'get_text', self._set_called)
3523-
3524- self.ui.set_new_password_ok_button.set_sensitive(False)
3525- self.ui.set_new_password_ok_button.clicked()
3526- self.assertFalse(self._called)
3527-
3528- self.ui.set_new_password_ok_button.set_sensitive(True)
3529- self.ui.set_new_password_ok_button.clicked()
3530- self.assertTrue(self._called)
3531-
3532-
3533-class SetNewPasswordValidationTestCase(UbuntuSSOClientTestCase):
3534- """Test suite for validations for setting a new password."""
3535-
3536- def test_warning_is_shown_if_reset_code_empty(self):
3537- """A warning message is shown if reset_code is empty."""
3538- self.ui.reset_code_entry.set_text('')
3539-
3540- self.ui.set_new_password_ok_button.set_sensitive(True)
3541- self.ui.set_new_password_ok_button.clicked()
3542-
3543- self.assert_correct_entry_warning(self.ui.reset_code_entry,
3544- gui.FIELD_REQUIRED)
3545- self.assertNotIn('set_new_password', self.ui.backend._called)
3546-
3547- def test_password_help_is_always_shown(self):
3548- """Password help text is correctly displayed."""
3549- visible = self.ui.reset_password_help_label.get_property('visible')
3550- self.assertTrue(visible, 'password help text is visible.')
3551- actual = self.ui.reset_password_help_label.get_text()
3552- self.assertEqual(actual.decode('utf8'), gui.PASSWORD_HELP)
3553- self.assertNotIn('set_new_password', self.ui.backend._called)
3554-
3555- def test_warning_is_shown_if_password_mismatch(self):
3556- """A warning message is shown if password doesn't match."""
3557- self.ui.reset_password1_entry.set_text(PASSWORD)
3558- self.ui.reset_password2_entry.set_text(PASSWORD * 2)
3559-
3560- self.ui.set_new_password_ok_button.set_sensitive(True)
3561- self.ui.set_new_password_ok_button.clicked()
3562-
3563- self.assert_correct_entry_warning(self.ui.reset_password1_entry,
3564- gui.PASSWORD_MISMATCH)
3565- self.assert_correct_entry_warning(self.ui.reset_password2_entry,
3566- gui.PASSWORD_MISMATCH)
3567- self.assertNotIn('set_new_password', self.ui.backend._called)
3568-
3569- def test_warning_is_shown_if_password_too_weak(self):
3570- """A warning message is shown if password is too weak."""
3571- # password will match but will be too weak
3572- for pwd in ('', 'h3lloWo', PASSWORD.lower(), 'helloWorld'):
3573- self.ui.reset_password1_entry.set_text(pwd)
3574- self.ui.reset_password2_entry.set_text(pwd)
3575-
3576- self.ui.set_new_password_ok_button.set_sensitive(True)
3577- self.ui.set_new_password_ok_button.clicked()
3578-
3579- self.assert_correct_entry_warning(self.ui.reset_password1_entry,
3580- gui.PASSWORD_TOO_WEAK)
3581- self.assert_correct_entry_warning(self.ui.reset_password2_entry,
3582- gui.PASSWORD_TOO_WEAK)
3583- self.assertNotIn('set_new_password', self.ui.backend._called)
3584-
3585- def test_no_warning_messages_if_valid_data(self):
3586- """No warning messages are shown if the data is valid."""
3587- # this will certainly NOT generate warnings
3588- self.click_set_new_password_with_valid_data()
3589-
3590- self.assert_warnings_visibility()
3591-
3592- def test_no_warning_messages_if_valid_data_after_invalid_data(self):
3593- """No warnings if the data is valid (with prior invalid data)."""
3594- # this will certainly generate warnings
3595- self.ui.set_new_password_ok_button.clicked()
3596-
3597- # this will certainly NOT generate warnings
3598- self.click_set_new_password_with_valid_data()
3599-
3600- self.assert_warnings_visibility()
3601-
3602-
3603-class SignalsTestCase(UbuntuSSOClientTestCase):
3604- """Test suite for the backend signals."""
3605-
3606- def test_all_the_signals_are_listed(self):
3607- """All the backend signals are listed to be bound."""
3608- for sig in ('CaptchaGenerated', 'CaptchaGenerationError',
3609- 'UserRegistered', 'UserRegistrationError',
3610- 'LoggedIn', 'LoginError', 'UserNotValidated',
3611- 'EmailValidated', 'EmailValidationError',
3612- 'PasswordResetTokenSent', 'PasswordResetError',
3613- 'PasswordChanged', 'PasswordChangeError'):
3614- self.assertIn(sig, self.ui._signals)
3615-
3616- def test_signal_receivers_are_connected(self):
3617- """Callbacks are connected to signals of interest."""
3618- msg1 = 'callback %r for signal %r must be added to the backend.'
3619- msg2 = 'callback %r for signal %r must be added to the ui log.'
3620- for signal, method in self.ui._signals.items():
3621- actual = self.ui.backend.callbacks.get(signal)
3622- self.assertEqual([method], actual, msg1 % (method, signal))
3623- actual = self.ui._signals_receivers.get(signal)
3624- self.assertEqual(method, actual, msg2 % (method, signal))
3625-
3626- def test_callbacks_only_log_when_app_name_doesnt_match(self):
3627- """Callbacks do nothing but logging when app_name doesn't match."""
3628- mismatch_app_name = self.ui.app_name * 2
3629- for method in self.ui._signals.values():
3630- msgs = ('ignoring', method.__name__, repr(mismatch_app_name))
3631- method(mismatch_app_name, 'dummy')
3632- self.assertTrue(self.memento.check(logging.INFO, *msgs))
3633- self.memento.reset()
3634-
3635- def test_on_captcha_generated_is_not_called(self):
3636- """on_captcha_generated is not called if incorrect app_name."""
3637- self.patch(self.ui, 'on_captcha_generated', self._set_called)
3638- mismatch_app_name = self.ui.app_name * 2
3639- self.ui._signals['CaptchaGenerated'](mismatch_app_name, 'dummy')
3640- self.assertFalse(self._called)
3641-
3642- def test_on_captcha_generation_error_is_not_called(self):
3643- """on_captcha_generation_error is not called if incorrect app_name."""
3644- self.patch(self.ui, 'on_captcha_generation_error', self._set_called)
3645- mismatch_app_name = self.ui.app_name * 2
3646- self.ui._signals['CaptchaGenerationError'](mismatch_app_name, 'dummy')
3647- self.assertFalse(self._called)
3648-
3649- def test_on_user_registered_is_not_called(self):
3650- """on_user_registered is not called if incorrect app_name."""
3651- self.patch(self.ui, 'on_user_registered', self._set_called)
3652- mismatch_app_name = self.ui.app_name * 2
3653- self.ui._signals['UserRegistered'](mismatch_app_name, 'dummy')
3654- self.assertFalse(self._called)
3655-
3656- def test_on_user_registration_error_is_not_called(self):
3657- """on_user_registration_error is not called if incorrect app_name."""
3658- self.patch(self.ui, 'on_user_registration_error', self._set_called)
3659- mismatch_app_name = self.ui.app_name * 2
3660- self.ui._signals['UserRegistrationError'](mismatch_app_name, 'dummy')
3661- self.assertFalse(self._called)
3662-
3663- def test_on_email_validated_is_not_called(self):
3664- """on_email_validated is not called if incorrect app_name."""
3665- self.patch(self.ui, 'on_email_validated', self._set_called)
3666- mismatch_app_name = self.ui.app_name * 2
3667- self.ui._signals['EmailValidated'](mismatch_app_name, 'dummy')
3668- self.assertFalse(self._called)
3669-
3670- def test_on_email_validation_error_is_not_called(self):
3671- """on_email_validation_error is not called if incorrect app_name."""
3672- self.patch(self.ui, 'on_email_validation_error', self._set_called)
3673- mismatch_app_name = self.ui.app_name * 2
3674- self.ui._signals['EmailValidationError'](mismatch_app_name, 'dummy')
3675- self.assertFalse(self._called)
3676-
3677- def test_on_logged_in_is_not_called(self):
3678- """on_logged_in is not called if incorrect app_name."""
3679- self.patch(self.ui, 'on_logged_in', self._set_called)
3680- mismatch_app_name = self.ui.app_name * 2
3681- self.ui._signals['LoggedIn'](mismatch_app_name, 'dummy')
3682- self.assertFalse(self._called)
3683-
3684- def test_on_login_error_is_not_called(self):
3685- """on_login_error is not called if incorrect app_name."""
3686- self.patch(self.ui, 'on_login_error', self._set_called)
3687- mismatch_app_name = self.ui.app_name * 2
3688- self.ui._signals['LoginError'](mismatch_app_name, 'dummy')
3689- self.assertFalse(self._called)
3690-
3691- def test_on_user_not_validated_is_not_called(self):
3692- """on_user_not_validated is not called if incorrect app_name."""
3693- self.patch(self.ui, 'on_user_not_validated', self._set_called)
3694- mismatch_app_name = self.ui.app_name * 2
3695- self.ui._signals['UserNotValidated'](mismatch_app_name, 'dummy')
3696- self.assertFalse(self._called)
3697-
3698- def test_on_password_reset_token_sent_is_not_called(self):
3699- """on_password_reset_token_sent is not called if incorrect app_name."""
3700- self.patch(self.ui, 'on_password_reset_token_sent', self._set_called)
3701- mismatch_app_name = self.ui.app_name * 2
3702- self.ui._signals['PasswordResetTokenSent'](mismatch_app_name, 'dummy')
3703- self.assertFalse(self._called)
3704-
3705- def test_on_password_reset_error_is_not_called(self):
3706- """on_password_reset_error is not called if incorrect app_name."""
3707- self.patch(self.ui, 'on_password_reset_error', self._set_called)
3708- mismatch_app_name = self.ui.app_name * 2
3709- self.ui._signals['PasswordResetError'](mismatch_app_name, 'dummy')
3710- self.assertFalse(self._called)
3711-
3712- def test_on_password_changed_is_not_called(self):
3713- """on_password_changed is not called if incorrect app_name."""
3714- self.patch(self.ui, 'on_password_changed', self._set_called)
3715- mismatch_app_name = self.ui.app_name * 2
3716- self.ui._signals['PasswordChanged'](mismatch_app_name, 'dummy')
3717- self.assertFalse(self._called)
3718-
3719- def test_on_password_change_error_is_not_called(self):
3720- """on_password_change_error is not called if incorrect app_name."""
3721- self.patch(self.ui, 'on_password_change_error', self._set_called)
3722- mismatch_app_name = self.ui.app_name * 2
3723- self.ui._signals['PasswordChangeError'](mismatch_app_name, 'dummy')
3724- self.assertFalse(self._called)
3725-
3726-
3727-class LoginOnlyTestCase(UbuntuSSOClientTestCase):
3728- """Test suite for the login only GUI."""
3729-
3730- kwargs = dict(app_name=APP_NAME, tc_url=None, help_text=HELP_TEXT,
3731- login_only=True)
3732-
3733- def test_login_is_first_page(self):
3734- """When starting, the login page is the first one."""
3735- self.assert_pages_visibility(login=True)
3736-
3737- def test_no_back_button(self):
3738- """There is no back button in the login screen."""
3739- self.assertFalse(self.ui.login_back_button.get_property('visible'))
3740-
3741- def test_login_ok_button_has_the_focus(self):
3742- """The login_ok_button has the focus."""
3743- self.assertTrue(self.ui.login_ok_button.is_focus())
3744-
3745- def test_help_text_is_used(self):
3746- """The passed help_text is used."""
3747- self.assertEqual(self.ui.help_label.get_text().decode('utf8'),
3748- HELP_TEXT)
3749-
3750-
3751-class ReturnCodeTestCase(UbuntuSSOClientTestCase):
3752- """Test the return codes."""
3753-
3754- def setUp(self):
3755- super(ReturnCodeTestCase, self).setUp()
3756- self.patch(gui.sys, 'exit', self._set_called)
3757-
3758- def test_closing_main_window(self):
3759- """When closing the main window, USER_CANCELLATION is called."""
3760- self.ui.window.emit('delete-event', Gdk.Event())
3761- self.assertEqual(self._called, ((gui.USER_CANCELLATION,), {}))
3762-
3763- def test_every_cancel_calls_proper_callback(self):
3764- """When any cancel button is clicked, USER_CANCELLATION is called."""
3765- self.patch(self.ui.backend, 'disconnect_from_signal', lambda *a: None)
3766- msg = 'USER_CANCELLATION should be returned when %r is clicked.'
3767- buttons = filter(lambda name: 'cancel_button' in name, self.ui.widgets)
3768- for name in buttons:
3769- widget = getattr(self.ui, name)
3770- widget.clicked()
3771- self.assertEqual(self._called, ((gui.USER_CANCELLATION,), {}),
3772- msg % name)
3773- self._called = False
3774-
3775- def test_on_user_registration_error_proper_callback_is_called(self):
3776- """On UserRegistrationError, USER_CANCELLATION is called."""
3777- self.ui.on_user_registration_error(app_name=APP_NAME, error=self.error)
3778- self.ui.on_close_clicked()
3779-
3780- self.assertEqual(self._called, ((gui.USER_CANCELLATION,), {}))
3781-
3782- def test_on_email_validated_proper_callback_is_called(self):
3783- """On EmailValidated, REGISTRATION_SUCCESS is called."""
3784- self.ui.on_email_validated(app_name=APP_NAME, email=EMAIL)
3785- self.ui.on_close_clicked()
3786-
3787- self.assertEqual(self._called, ((gui.USER_SUCCESS,), {}))
3788-
3789- def test_on_email_validation_error_proper_callback_is_called(self):
3790- """On EmailValidationError, USER_CANCELLATION is called."""
3791- self.ui.on_email_validation_error(app_name=APP_NAME, error=self.error)
3792- self.ui.on_close_clicked()
3793-
3794- self.assertEqual(self._called, ((gui.USER_CANCELLATION,), {}))
3795-
3796- def test_on_logged_in_proper_callback_is_called(self):
3797- """On LoggedIn, LOGIN_SUCCESS is called."""
3798- self.ui.on_logged_in(app_name=APP_NAME, email=EMAIL)
3799- self.ui.on_close_clicked()
3800-
3801- self.assertEqual(self._called, ((gui.USER_SUCCESS,), {}))
3802-
3803- def test_on_login_error_proper_callback_is_called(self):
3804- """On LoginError, USER_CANCELLATION is called."""
3805- self.click_connect_with_valid_data()
3806- self.ui.on_login_error(app_name=APP_NAME, error=self.error)
3807- self.ui.on_close_clicked()
3808-
3809- self.assertEqual(self._called, ((gui.USER_CANCELLATION,), {}))
3810-
3811- def test_registration_success_even_if_prior_registration_error(self):
3812- """Only one callback is called with the final outcome.
3813-
3814- When the user successfully registers, REGISTRATION_SUCCESS is
3815- called even if there were errors before.
3816-
3817- """
3818- self.click_join_with_valid_data()
3819- self.ui.on_user_registration_error(app_name=APP_NAME, error=self.error)
3820- self.click_join_with_valid_data()
3821- self.ui.on_email_validated(app_name=APP_NAME, email=EMAIL)
3822- self.ui.on_close_clicked()
3823-
3824- self.assertEqual(self._called, ((gui.USER_SUCCESS,), {}))
3825-
3826- def test_login_success_even_if_prior_login_error(self):
3827- """Only one callback is called with the final outcome.
3828-
3829- When the user successfully logs in, LOGIN_SUCCESS is called even if
3830- there were errors before.
3831-
3832- """
3833- self.click_connect_with_valid_data()
3834- self.ui.on_login_error(app_name=APP_NAME, error=self.error)
3835- self.click_connect_with_valid_data()
3836- self.ui.on_logged_in(app_name=APP_NAME, email=EMAIL)
3837- self.ui.on_close_clicked()
3838-
3839- self.assertEqual(self._called, ((gui.USER_SUCCESS,), {}))
3840-
3841- def test_user_cancelation_even_if_prior_registration_error(self):
3842- """Only one callback is called with the final outcome.
3843-
3844- When the user closes the window, USER_CANCELLATION is called even if
3845- there were registration errors before.
3846-
3847- """
3848- self.click_join_with_valid_data()
3849- self.ui.on_user_registration_error(app_name=APP_NAME, error=self.error)
3850- self.ui.join_cancel_button.clicked()
3851-
3852- self.assertEqual(self._called, ((gui.USER_CANCELLATION,), {}))
3853-
3854- def test_user_cancelation_even_if_prior_login_error(self):
3855- """Only one callback is called with the final outcome.
3856-
3857- When the user closes the window, USER_CANCELLATION is called even if
3858- there were login errors before.
3859-
3860- """
3861- self.click_connect_with_valid_data()
3862- self.ui.on_login_error(app_name=APP_NAME, error=self.error)
3863- self.ui.login_cancel_button.clicked()
3864-
3865- self.assertEqual(self._called, ((gui.USER_CANCELLATION,), {}))
3866-
3867-
3868-class DefaultButtonsTestCase(UbuntuSSOClientTestCase):
3869- """Each UI page has a default button when visible."""
3870-
3871- def setUp(self):
3872- """Init."""
3873- super(DefaultButtonsTestCase, self).setUp()
3874- self.mapping = (
3875- ('enter_details_vbox', 'join_ok_button'),
3876- ('tc_browser_vbox', 'tc_back_button'),
3877- ('verify_email_vbox', 'verify_token_button'),
3878- ('login_vbox', 'login_ok_button'),
3879- ('request_password_token_vbox',
3880- 'request_password_token_ok_button'),
3881- ('set_new_password_vbox', 'set_new_password_ok_button'),
3882- ('success_vbox', 'finish_close_button'),
3883- ('error_vbox', 'finish_close_button'),
3884- ('processing_vbox', None))
3885-
3886- def test_pages_have_default_widget_set(self):
3887- """Each page has a proper button as default widget."""
3888- msg = 'Page %r must have %r as default_widget (got %r instead).'
3889- for pname, bname in self.mapping:
3890- page = getattr(self.ui, pname)
3891- button = bname and getattr(self.ui, bname)
3892- self.assertTrue(page.default_widget is button,
3893- msg % (pname, bname, page.default_widget))
3894-
3895- def test_default_widget_can_default(self):
3896- """Each default button can default."""
3897- msg = 'Button %r must have can-default enabled.'
3898- for _, bname in self.mapping:
3899- if bname is not None:
3900- button = getattr(self.ui, bname)
3901- self.assertTrue(button.get_property('can-default'),
3902- msg % bname)
3903-
3904- def test_set_current_page_grabs_focus_for_default_button(self):
3905- """Setting the current page marks the default widget as default."""
3906- msg = '%r "has_default" must be True when %r if the current page.'
3907- for pname, bname in self.mapping:
3908- if bname is not None:
3909- page = getattr(self.ui, pname)
3910- self.patch(page.default_widget, 'grab_default',
3911- self._set_called)
3912- self.ui._set_current_page(page)
3913- self.assertEqual(self._called, ((), {}), msg % (bname, pname))
3914- self._called = False
3915-
3916-
3917-class RunTestCase(BasicTestCase):
3918-
3919- def test_run(self):
3920- """Calling run.gui() a UI instance is created."""
3921- called = []
3922- self.patch(gui, 'UbuntuSSOClientGUI',
3923- lambda **kw: called.append(('GUI', kw)))
3924- self.patch(gui.Gtk, 'main',
3925- lambda: called.append('main'))
3926-
3927- kwargs = dict(foo='foo', bar='bar', baz='yadda', yadda=0)
3928- gui.run(**kwargs)
3929-
3930- kwargs['close_callback'] = gui.Gtk.main_quit
3931- self.assertEqual(called, [('GUI', kwargs), 'main'])
3932
3933=== modified file 'softwarecenter/ui/gtk3/panes/availablepane.py'
3934--- softwarecenter/ui/gtk3/panes/availablepane.py 2012-12-14 16:44:25 +0000
3935+++ softwarecenter/ui/gtk3/panes/availablepane.py 2013-09-17 17:52:06 +0000
3936@@ -37,6 +37,7 @@
3937 NonAppVisibility,
3938 DEFAULT_SEARCH_LIMIT,
3939 TransactionTypes,
3940+ PURCHASE_TRANSACTION_ID,
3941 )
3942 from softwarecenter.utils import (
3943 convert_desktop_file_to_installed_location,
3944@@ -58,8 +59,8 @@
3945
3946 from softwarecenter.backend.channel import SoftwareChannel
3947 from softwarecenter.backend.unitylauncher import (UnityLauncher,
3948- UnityLauncherInfo,
3949- TransactionDetails)
3950+ UnityLauncherInfo)
3951+from softwarecenter.backend.zeitgeist_logger import ZeitgeistLogger
3952
3953 LOG = logging.getLogger(__name__)
3954
3955@@ -91,6 +92,58 @@
3956 None,
3957 ())}
3958
3959+ class TransactionDetails(object):
3960+ """ Simple class to keep track of aptdaemon transaction details """
3961+ def __init__(self, db, pkgname, appname, trans_id, trans_type):
3962+ self.db = db
3963+ self.app = Application(pkgname=pkgname, appname=appname)
3964+ self.trans_id = trans_id
3965+ self.trans_type = trans_type
3966+ self.__app_details = None
3967+ self.__real_desktop = None
3968+
3969+ if trans_type != TransactionTypes.INSTALL:
3970+ self.guess_final_desktop_file()
3971+
3972+ @property
3973+ def app_details(self):
3974+ if not self.__app_details:
3975+ self.__app_details = self.app.get_details(self.db)
3976+ return self.__app_details
3977+
3978+ @property
3979+ def desktop_file(self):
3980+ return self.app_details.desktop_file
3981+
3982+ @property
3983+ def final_desktop_file(self):
3984+ return self.guess_final_desktop_file()
3985+
3986+ def guess_final_desktop_file(self):
3987+ if self.__real_desktop:
3988+ return self.__real_desktop
3989+
3990+ # convert the app-install desktop file location to the actual installed
3991+ # desktop file location (or in the case of a purchased item from the
3992+ # agent, generate the correct installed desktop file location)
3993+ desktop_file = (
3994+ convert_desktop_file_to_installed_location(self.desktop_file,
3995+ self.app.pkgname))
3996+ # we only add items to the launcher that have a desktop file
3997+ if not desktop_file:
3998+ return
3999+ # do not add apps that have no Exec entry in their desktop file
4000+ # (e.g. wine, see LP: #848437 or ubuntu-restricted-extras,
4001+ # see LP: #913756), also, don't add the item if NoDisplay is
4002+ # specified (see LP: #1006483)
4003+ if (os.path.exists(desktop_file) and
4004+ (not get_exec_line_from_desktop(desktop_file) or
4005+ is_no_display_desktop_file(desktop_file))):
4006+ return
4007+
4008+ self.__real_desktop = desktop_file
4009+ return self.__real_desktop
4010+
4011 def __init__(self,
4012 cache,
4013 db,
4014@@ -118,9 +171,9 @@
4015
4016 # integrate with the Unity launcher
4017 self.unity_launcher = UnityLauncher()
4018- # keep track of applications that are queued to be added
4019- # to the Unity launcher
4020- self.unity_launcher_transaction_queue = {}
4021+
4022+ # keep track of transactions
4023+ self.transactions_queue = {}
4024
4025 def init_view(self):
4026 if self.view_initialized:
4027@@ -212,17 +265,18 @@
4028 Gtk.Label(label=NavButtons.PURCHASE))
4029
4030 # install backend
4031+ # FIXME: move this out of the available pane really
4032 self.backend.connect("transaction-started",
4033 self.on_transaction_started)
4034 self.backend.connect("transactions-changed",
4035 self.on_transactions_changed)
4036 self.backend.connect("transaction-finished",
4037 self.on_transaction_complete)
4038- self.backend.connect("transaction-cancelled",
4039- self.on_transaction_cancelled)
4040 # a transaction error is treated the same as a cancellation
4041 self.backend.connect("transaction-stopped",
4042 self.on_transaction_cancelled)
4043+ self.backend.connect("transaction-cancelled",
4044+ self.on_transaction_cancelled)
4045
4046 # now we are initialized
4047 self.searchentry.set_sensitive(True)
4048@@ -375,8 +429,23 @@
4049
4050 def on_transaction_started(self, backend, pkgname, appname, trans_id,
4051 trans_type):
4052- self._register_unity_launcher_transaction_started(pkgname, appname,
4053- trans_id, trans_type)
4054+ details = self.TransactionDetails(self.db, pkgname, appname, trans_id, trans_type)
4055+ self.transactions_queue[pkgname] = details
4056+
4057+ config = get_config()
4058+ if (trans_type == TransactionTypes.INSTALL and
4059+ trans_id != PURCHASE_TRANSACTION_ID and
4060+ config.add_to_unity_launcher and
4061+ softwarecenter.utils.is_unity_running()):
4062+ self._add_application_to_unity_launcher(details)
4063+
4064+ def on_transaction_cancelled(self, backend, result):
4065+ """ handle a transaction that has been cancelled
4066+ """
4067+ if result.pkgname:
4068+ self.unity_launcher.cancel_application_to_launcher(result.pkgname)
4069+ if result.pkgname in self.transactions_queue:
4070+ self.transactions_queue.pop(result.pkgname)
4071
4072 def on_transactions_changed(self, backend, pending_transactions):
4073 """internal helper that keeps the action bar up-to-date by
4074@@ -385,92 +454,47 @@
4075 if self._is_custom_list_search(self.state.search_term):
4076 self._update_action_bar()
4077
4078- def on_transaction_complete(self, backend, result):
4079- """ handle a transaction that has completed successfully
4080- """
4081- if result.pkgname in self.unity_launcher_transaction_queue:
4082- transaction_details = (
4083- self.unity_launcher_transaction_queue.pop(result.pkgname))
4084- self._add_application_to_unity_launcher(transaction_details)
4085-
4086- def on_transaction_cancelled(self, backend, result):
4087- """ handle a transaction that has been cancelled
4088- """
4089- if result.pkgname in self.unity_launcher_transaction_queue:
4090- self.unity_launcher_transaction_queue.pop(result.pkgname)
4091-
4092- def _register_unity_launcher_transaction_started(self, pkgname, appname,
4093- trans_id, trans_type):
4094- # at the start of the transaction, we gather details for use later
4095- # when it is time to add the installed app to the Unity launcher,
4096- # (see #972710). we only care about installs for the launcher
4097- # mvo: use softwarecenter.utils explicitly here so that we can monkey
4098- # patch it in the test
4099- config = get_config()
4100- if (trans_type == TransactionTypes.INSTALL and
4101- config.add_to_unity_launcher and
4102- softwarecenter.utils.is_unity_running()):
4103- transaction_details = TransactionDetails(
4104- pkgname, appname, trans_id, trans_type)
4105- self.unity_launcher_transaction_queue[pkgname] = (
4106- transaction_details)
4107-
4108- def _add_application_to_unity_launcher(self, transaction_details):
4109- app = Application(pkgname=transaction_details.pkgname,
4110- appname=transaction_details.appname)
4111- appdetails = app.get_details(self.db)
4112- # convert the app-install desktop file location to the actual installed
4113- # desktop file location (or in the case of a purchased item from the
4114- # agent, generate the correct installed desktop file location)
4115- installed_desktop_file_path = (
4116- convert_desktop_file_to_installed_location(appdetails.desktop_file,
4117- app.pkgname))
4118- # we only add items to the launcher that have a desktop file
4119- if not installed_desktop_file_path:
4120- return
4121+ def _add_application_to_unity_launcher(self, trans_details):
4122 # do not add apps that have no Exec entry in their desktop file
4123 # (e.g. wine, see LP: #848437 or ubuntu-restricted-extras,
4124 # see LP: #913756), also, don't add the item if NoDisplay is
4125 # specified (see LP: #1006483)
4126- if (os.path.exists(installed_desktop_file_path) and
4127- (not get_exec_line_from_desktop(installed_desktop_file_path) or
4128- is_no_display_desktop_file(installed_desktop_file_path))):
4129+ if (os.path.exists(trans_details.desktop_file) and
4130+ (not get_exec_line_from_desktop(trans_details.desktop_file) or
4131+ is_no_display_desktop_file(trans_details.desktop_file))):
4132 return
4133
4134 # now gather up the unity launcher info items and send the app to the
4135 # launcher service
4136- launcher_info = self._get_unity_launcher_info(app, appdetails,
4137- installed_desktop_file_path,
4138- transaction_details.trans_id)
4139+ launcher_info = self._get_unity_launcher_info(trans_details)
4140 self.unity_launcher.send_application_to_launcher(
4141- transaction_details.pkgname,
4142- launcher_info)
4143-
4144- def _get_unity_launcher_info(
4145- self, app, appdetails, installed_desktop_file_path, trans_id):
4146+ trans_details.app.pkgname, launcher_info)
4147+
4148+ def on_transaction_complete(self, backend, result):
4149+ """ handle a transaction that has completed successfully
4150+ """
4151+ if result.pkgname in self.transactions_queue:
4152+ details = self.transactions_queue.pop(result.pkgname)
4153+
4154+ if details.trans_type == TransactionTypes.INSTALL:
4155+ ZeitgeistLogger(self.distro).log_install_event(details.final_desktop_file)
4156+ elif details.trans_type == TransactionTypes.REMOVE:
4157+ ZeitgeistLogger(self.distro).log_uninstall_event(details.final_desktop_file)
4158+
4159+ def _get_unity_launcher_info(self, trans_details):
4160 (icon_size, icon_x, icon_y) = (
4161- self._get_onscreen_icon_details_for_launcher_service(app))
4162+ self._get_onscreen_icon_details_for_launcher_service(trans_details.app))
4163 icon_path = get_file_path_from_iconname(
4164 self.icons,
4165- iconname=appdetails.icon)
4166- # Note that the transaction ID is not specified because we are firing
4167- # the dbus signal at the very end of the installation now, and the
4168- # corresponding fix in Unity is expecting this value to be empty (it
4169- # would not be useful to the Unity launcher at this point anyway,
4170- # as the corresponding transaction would be complete).
4171- # Please see bug LP: #925014 and corresponding Unity branch for
4172- # further details.
4173- # TODO: If and when we re-implement firing the dbus signal at the
4174- # start of the transaction, at that time we will need to replace
4175- # the empty string below with the trans_id value.
4176- launcher_info = UnityLauncherInfo(app.name,
4177- appdetails.icon,
4178+ iconname=trans_details.app_details.icon)
4179+ launcher_info = UnityLauncherInfo(trans_details.app.name,
4180+ trans_details.app_details.icon,
4181 icon_path,
4182 icon_x,
4183 icon_y,
4184 icon_size,
4185- installed_desktop_file_path,
4186- "")
4187+ trans_details.desktop_file,
4188+ trans_details.trans_id)
4189 return launcher_info
4190
4191 def _get_onscreen_icon_details_for_launcher_service(self, app):
4192
4193=== modified file 'softwarecenter/ui/gtk3/widgets/stars.py'
4194--- softwarecenter/ui/gtk3/widgets/stars.py 2012-11-28 17:23:44 +0000
4195+++ softwarecenter/ui/gtk3/widgets/stars.py 2013-09-17 17:52:06 +0000
4196@@ -49,7 +49,7 @@
4197 REACTIVE = -1
4198
4199
4200-class ShapeStar():
4201+class ShapeStar(object):
4202 def __init__(self, points, indent=0.61):
4203 self.coords = self._calc_coords(points, 1 - indent)
4204
4205
4206=== modified file 'softwarecenter/utils.py'
4207--- softwarecenter/utils.py 2013-07-09 14:31:33 +0000
4208+++ softwarecenter/utils.py 2013-09-17 17:52:06 +0000
4209@@ -511,20 +511,32 @@
4210 # lastly, just try checking directly for the desktop file based on the
4211 # pkgname itself for the case of for-purchase items, etc.
4212 if pkgname:
4213- installed_desktop_file_path = "/usr/share/applications/%s.desktop" %\
4214- pkgname
4215- if os.path.exists(installed_desktop_file_path):
4216- return installed_desktop_file_path
4217+ datadirs = GLib.get_system_data_dirs()
4218+ for dir in datadirs:
4219+ path = "%s/applications/%s.desktop" % (dir, pkgname)
4220+ if os.path.exists(path):
4221+ return path
4222+
4223 # files in the extras archive have their desktop filenames prepended
4224 # by "extras-", so we check for that also (LP: #1012877)
4225- extras_desktop_file_path = \
4226- "/usr/share/applications/extras-%s.desktop" % pkgname
4227- if os.path.exists(extras_desktop_file_path):
4228- return extras_desktop_file_path
4229+ for dir in datadirs:
4230+ path = "%s/applications/extras-%s.desktop" % (dir, pkgname)
4231+ if os.path.exists(path):
4232+ return path
4233 LOG.warn("Could not determine the installed desktop file path for "
4234 "app-install desktop file: '%s'" % app_install_data_file_path)
4235 return ""
4236
4237+def get_desktop_id(desktop_file):
4238+ """ Gets a desktop-id from a .desktop file path"""
4239+ datadirs = [(i if i[-1] == '/' else i+'/') + 'applications/' for i in \
4240+ GLib.get_system_data_dirs() + [GLib.get_user_data_dir()]]
4241+
4242+ for dir in datadirs:
4243+ if desktop_file.startswith(dir):
4244+ return desktop_file[len(dir):].replace('/', '-')
4245+
4246+ return desktop_file
4247
4248 def clear_token_from_ubuntu_sso_sync(appname):
4249 """ send a dbus signal to the com.ubuntu.sso service to clear
4250
4251=== modified file 'tests/gtk3/test_navhistory.py'
4252--- tests/gtk3/test_navhistory.py 2012-08-22 06:49:53 +0000
4253+++ tests/gtk3/test_navhistory.py 2013-09-17 17:52:06 +0000
4254@@ -12,7 +12,7 @@
4255 from softwarecenter.ui.gtk3.session.displaystate import DisplayState
4256
4257
4258-class MockButton():
4259+class MockButton(object):
4260
4261 def __init__(self):
4262 self.sensitive = True
4263
4264=== renamed file 'tests/gtk3/test_unity_launcher_integration.py' => 'tests/gtk3/test_unity_launcher_integration_gui.py'
4265--- tests/gtk3/test_unity_launcher_integration.py 2012-09-18 08:36:43 +0000
4266+++ tests/gtk3/test_unity_launcher_integration_gui.py 2013-09-17 17:52:06 +0000
4267@@ -1,11 +1,9 @@
4268-import os
4269 import unittest
4270
4271 from gi.repository import Gtk
4272 from mock import Mock, patch
4273
4274 from tests.utils import (
4275- DATA_DIR,
4276 do_events,
4277 do_events_with_sleep,
4278 setup_test_env,
4279@@ -19,14 +17,13 @@
4280
4281 from softwarecenter.enums import TransactionTypes
4282 from softwarecenter.ui.gtk3.panes.availablepane import AvailablePane
4283-from softwarecenter.utils import convert_desktop_file_to_installed_location
4284 from tests.gtk3.windows import get_test_window_availablepane
4285
4286 # Tests for Ubuntu Software Center's integration with the Unity launcher,
4287 # see https://wiki.ubuntu.com/SoftwareCenter#Learning%20how%20to%20launch%20an%20application
4288
4289
4290-class TestUnityLauncherIntegration(unittest.TestCase):
4291+class TestUnityLauncherIntegrationGUI(unittest.TestCase):
4292
4293 @classmethod
4294 def setUpClass(cls):
4295@@ -39,9 +36,9 @@
4296
4297 # patch is_unity_running so that the test works inside a e.g.
4298 # ADT
4299- patcher = patch("softwarecenter.utils.is_unity_running")
4300- mock = patcher.start()
4301- mock.return_value = True
4302+ patch("softwarecenter.utils.is_unity_running").start().return_value = True
4303+ patch("softwarecenter.backend.unitylauncher.UnityLauncher"
4304+ "._get_launcher_dbus_iface").start().return_value = Mock()
4305
4306 @classmethod
4307 def tearDownClass(cls):
4308@@ -117,15 +114,13 @@
4309 self.expected_launcher_info = UnityLauncherInfo("software-center",
4310 "softwarecenter",
4311 0, 0, 0, 0, # these values are set in availablepane
4312- "/usr/share/applications/ubuntu-software-center.desktop",
4313- "")
4314+ "/usr/share/app-install/desktop/software-center:ubuntu-software-center.desktop",
4315+ "testid101")
4316 self._install_from_list_view(test_pkgname)
4317 self.assertTrue(mock_send_application_to_launcher.called)
4318 args, kwargs = mock_send_application_to_launcher.call_args
4319 self._check_send_application_to_launcher_args(*args, **kwargs)
4320- # insure that the unity launcher transaction queue is cleared
4321- self.assertEqual(
4322- self.available_pane.unity_launcher_transaction_queue, {})
4323+
4324
4325 @patch('softwarecenter.ui.gtk3.panes.availablepane.UnityLauncher'
4326 '.send_application_to_launcher')
4327@@ -139,16 +134,13 @@
4328 self.expected_launcher_info = UnityLauncherInfo("software-center",
4329 "softwarecenter",
4330 0, 0, 0, 0, # these values are set in availablepane
4331- "/usr/share/applications/ubuntu-software-center.desktop",
4332- "")
4333+ "/usr/share/app-install/desktop/software-center:ubuntu-software-center.desktop",
4334+ "testid101")
4335 self._navigate_to_appdetails_and_install(test_pkgname)
4336 self.assertTrue(mock_send_application_to_launcher.called)
4337 args, kwargs = mock_send_application_to_launcher.call_args
4338 self._check_send_application_to_launcher_args(*args, **kwargs)
4339- # insure that the unity launcher transaction queue is cleared
4340- self.assertEqual(
4341- self.available_pane.unity_launcher_transaction_queue, {})
4342-
4343+
4344 @patch('softwarecenter.ui.gtk3.panes.availablepane.UnityLauncher'
4345 '.send_application_to_launcher')
4346 def test_unity_launcher_integration_disabled(self,
4347@@ -158,27 +150,18 @@
4348 test_pkgname = "software-center"
4349 self._navigate_to_appdetails_and_install(test_pkgname)
4350 self.assertFalse(mock_send_application_to_launcher.called)
4351- # insure that the unity launcher transaction queue is cleared
4352- self.assertEqual(
4353- self.available_pane.unity_launcher_transaction_queue, {})
4354-
4355- @patch('softwarecenter.ui.gtk3.panes.availablepane'
4356- '.convert_desktop_file_to_installed_location')
4357+
4358 @patch('softwarecenter.ui.gtk3.panes.availablepane.UnityLauncher'
4359 '.send_application_to_launcher')
4360- # NOTE: the order of attributes in the method call appears reversed, this
4361- # is because the patch decorators above are executed from innermost to
4362- # outermost
4363 def test_unity_launcher_integration_launcher(self,
4364- mock_send_application_to_launcher,
4365- mock_convert_desktop_file_to_installed_location):
4366+ mock_send_application_to_launcher):
4367 # this is a 3-tuple of (pkgname, desktop-file, expected_result)
4368 TEST_CASES = (
4369 # normal app
4370 ("software-center", "/usr/share/app-install/desktop/"\
4371 "software-center:ubuntu-software-center.desktop", True),
4372 # NoDisplay=True line
4373- ("wine", "/usr/share/app-install/desktop/"\
4374+ ("wine1.4", "/usr/share/app-install/desktop/"\
4375 "wine1.4:wine.desktop", False),
4376 # No Exec= line
4377 ("bzr", "/usr/share/app-install/desktop/"\
4378@@ -187,21 +170,23 @@
4379 # run the test over all test-cases
4380 self.config.add_to_unity_launcher = True
4381 for test_pkgname, app_install_desktop_file_path, res in TEST_CASES:
4382- # setup the mock
4383- mock_convert_desktop_file_to_installed_location.return_value = (
4384- app_install_desktop_file_path)
4385 # this is the tofu of the test
4386 self._navigate_to_appdetails_and_install(test_pkgname)
4387 # verify
4388- self.assertEqual(mock_send_application_to_launcher.called, res)
4389+ self.assertEqual(
4390+ mock_send_application_to_launcher.called,
4391+ res,
4392+ "expected %s for pkg: %s but got: %s" % (
4393+ res, test_pkgname,
4394+ mock_send_application_to_launcher.called))
4395 # and reset again to ensure we don't get the call info from
4396 # the previous call(s)
4397 mock_send_application_to_launcher.reset_mock()
4398
4399 @patch('softwarecenter.ui.gtk3.panes.availablepane.UnityLauncher'
4400- '.send_application_to_launcher')
4401+ '.cancel_application_to_launcher')
4402 def test_unity_launcher_integration_cancelled_install(self,
4403- mock_send_application_to_launcher):
4404+ mock_cancel_launcher):
4405 # test the automatic add to launcher enabled functionality when
4406 # installing an app from the details view, and then cancelling
4407 # the install (see LP: #1027209)
4408@@ -213,17 +198,14 @@
4409 do_events()
4410 self._simulate_install_events(app,
4411 result_event="transaction-cancelled")
4412- # ensure that we do not send the application to the launcher
4413- # on a cancelled installation
4414- self.assertFalse(mock_send_application_to_launcher.called)
4415- # insure that the unity launcher transaction queue is cleared
4416- self.assertEqual(
4417- self.available_pane.unity_launcher_transaction_queue, {})
4418-
4419+ # ensure that we cancel the send
4420+ self.assertTrue(
4421+ mock_cancel_launcher.called)
4422+
4423 @patch('softwarecenter.ui.gtk3.panes.availablepane.UnityLauncher'
4424- '.send_application_to_launcher')
4425+ '.cancel_application_to_launcher')
4426 def test_unity_launcher_integration_installation_failure(self,
4427- mock_send_application_to_launcher):
4428+ mock_cancel_launcher):
4429 # test the automatic add to launcher enabled functionality when
4430 # a failure is detected during the transaction (aptd emits a
4431 # "transaction-stopped" signal for this case)
4432@@ -237,52 +219,9 @@
4433 # error is encountered
4434 self._simulate_install_events(app,
4435 result_event="transaction-stopped")
4436- # ensure that we do not send the application to the launcher
4437- # on a failed installation
4438- self.assertFalse(mock_send_application_to_launcher.called)
4439- # insure that the unity launcher transaction queue is cleared
4440- self.assertEqual(
4441- self.available_pane.unity_launcher_transaction_queue, {})
4442-
4443-
4444-class DesktopFilepathConversionTestCase(unittest.TestCase):
4445-
4446- def test_normal(self):
4447- # test 'normal' case
4448- app_install_desktop_path = os.path.join(DATA_DIR, "app-install",
4449- "desktop", "deja-dup:deja-dup.desktop")
4450- installed_desktop_path = convert_desktop_file_to_installed_location(
4451- app_install_desktop_path, "deja-dup")
4452- self.assertEqual(installed_desktop_path, os.path.join(DATA_DIR,
4453- "applications", "deja-dup.desktop"))
4454-
4455- def test_encoded_subdir(self):
4456- # test encoded subdirectory case, e.g. e.g. kde4_soundkonverter.desktop
4457- app_install_desktop_path = os.path.join(DATA_DIR, "app-install",
4458- "desktop", "soundkonverter:kde4__soundkonverter.desktop")
4459- installed_desktop_path = convert_desktop_file_to_installed_location(
4460- app_install_desktop_path, "soundkonverter")
4461- self.assertEqual(installed_desktop_path, os.path.join(DATA_DIR,
4462- "applications", "kde4", "soundkonverter.desktop"))
4463-
4464- def test_purchase_via_software_center_agent(self):
4465- # test the for-purchase case (uses "software-center-agent" as its
4466- # appdetails.desktop_file value)
4467- # FIXME: this will only work if update-manager is installed
4468- app_install_desktop_path = "software-center-agent"
4469- installed_desktop_path = convert_desktop_file_to_installed_location(
4470- app_install_desktop_path, "update-manager")
4471- self.assertEqual(installed_desktop_path,
4472- "/usr/share/applications/update-manager.desktop")
4473-
4474- def test_no_value(self):
4475- # test case where we don't have a value for app_install_desktop_path
4476- # (e.g. for a local .deb install, see bug LP: #768158)
4477- installed_desktop_path = convert_desktop_file_to_installed_location(
4478- None, "update-manager")
4479- # FIXME: this will only work if update-manager is installed
4480- self.assertEqual(installed_desktop_path,
4481- "/usr/share/applications/update-manager.desktop")
4482+ # ensure that we cancel the send
4483+ self.assertTrue(
4484+ mock_cancel_launcher.called)
4485
4486
4487 if __name__ == "__main__":
4488
4489=== added file 'tests/gtk3/test_zeitgeist_logger_gui.py'
4490--- tests/gtk3/test_zeitgeist_logger_gui.py 1970-01-01 00:00:00 +0000
4491+++ tests/gtk3/test_zeitgeist_logger_gui.py 2013-09-17 17:52:06 +0000
4492@@ -0,0 +1,95 @@
4493+import unittest
4494+
4495+from mock import Mock, patch
4496+from tests.utils import setup_test_env
4497+
4498+setup_test_env()
4499+
4500+from softwarecenter.db.application import Application
4501+
4502+from softwarecenter.enums import TransactionTypes
4503+from tests.gtk3.windows import get_test_window_availablepane
4504+
4505+
4506+class TestZeitgeistLoggerGUI(unittest.TestCase):
4507+
4508+ @classmethod
4509+ def setUpClass(cls):
4510+ # we can only have one instance of availablepane, so create it here
4511+ cls.win = get_test_window_availablepane()
4512+ cls.available_pane = cls.win.get_data("pane")
4513+
4514+ @classmethod
4515+ def tearDownClass(cls):
4516+ cls.win.destroy()
4517+
4518+ def setUp(self):
4519+ zl = "softwarecenter.backend.zeitgeist_logger.ZeitgeistLogger";
4520+ self.log_install_event = patch(zl + ".log_install_event").start()
4521+ self.log_uninstall_event = patch(zl + ".log_uninstall_event").start()
4522+
4523+ def __emit_backend_event(self, pkgname, event):
4524+ mock_result = Mock()
4525+ mock_result.pkgname = pkgname
4526+ self.available_pane.backend.emit(event, mock_result)
4527+
4528+ def __start_backend_transaction(self, pkgname, type=TransactionTypes.INSTALL):
4529+ app = Application("", pkgname)
4530+ self.available_pane.backend.emit("transaction-started",
4531+ app.pkgname, app.appname,
4532+ "testid101", type)
4533+
4534+ def test_zeitgeist_logger_init_on_start(self):
4535+ test_pkgname = "software-center"
4536+ self.__start_backend_transaction(test_pkgname)
4537+ self.assertFalse(self.log_install_event.called)
4538+ self.assertFalse(self.log_uninstall_event.called)
4539+ self.assertEqual(len(self.available_pane.transactions_queue), 1)
4540+ self.assertTrue(test_pkgname in self.available_pane.transactions_queue)
4541+
4542+ def test_zeitgeist_logger_cancel_on_cancel(self):
4543+ test_pkgname = "software-center"
4544+ self.__start_backend_transaction(test_pkgname)
4545+ self.assertTrue(test_pkgname in self.available_pane.transactions_queue)
4546+
4547+ self.__emit_backend_event(test_pkgname, "transaction-cancelled")
4548+ self.assertEqual(len(self.available_pane.transactions_queue), 0)
4549+ self.assertFalse(self.log_install_event.called)
4550+ self.assertFalse(self.log_uninstall_event.called)
4551+
4552+ def test_zeitgeist_logger_cancel_on_stop(self):
4553+ test_pkgname = "software-center"
4554+ self.__start_backend_transaction(test_pkgname)
4555+ self.assertTrue(test_pkgname in self.available_pane.transactions_queue)
4556+
4557+ self.__emit_backend_event(test_pkgname, "transaction-stopped")
4558+ self.assertEqual(len(self.available_pane.transactions_queue), 0)
4559+ self.assertFalse(self.log_install_event.called)
4560+ self.assertFalse(self.log_uninstall_event.called)
4561+
4562+ def test_zeitgeist_logger_logs_install_on_finished(self):
4563+ test_pkgname = "software-center"
4564+ self.__start_backend_transaction(test_pkgname)
4565+ transaction = self.available_pane.transactions_queue[test_pkgname]
4566+
4567+ self.__emit_backend_event(test_pkgname, "transaction-finished")
4568+ self.assertTrue(self.log_install_event.called)
4569+ self.assertFalse(self.log_uninstall_event.called)
4570+ self.assertEqual(len(self.available_pane.transactions_queue), 0)
4571+ [args] = self.log_install_event.call_args[0]
4572+ self.assertEqual(args, transaction.real_desktop_file)
4573+
4574+ def test_zeitgeist_logger_logs_uninstall_on_finished(self):
4575+ test_pkgname = "software-center"
4576+ self.__start_backend_transaction(test_pkgname, TransactionTypes.REMOVE)
4577+ transaction = self.available_pane.transactions_queue[test_pkgname]
4578+
4579+ self.__emit_backend_event(test_pkgname, "transaction-finished")
4580+ self.assertFalse(self.log_install_event.called)
4581+ self.assertTrue(self.log_uninstall_event.called)
4582+ self.assertEqual(len(self.available_pane.transactions_queue), 0)
4583+ [args] = self.log_uninstall_event.call_args[0]
4584+ self.assertEqual(args, transaction.real_desktop_file)
4585+
4586+if __name__ == "__main__":
4587+ unittest.main()
4588
4589=== modified file 'tests/gtk3/windows.py'
4590--- tests/gtk3/windows.py 2012-11-28 15:54:20 +0000
4591+++ tests/gtk3/windows.py 2013-09-17 17:52:06 +0000
4592@@ -8,7 +8,7 @@
4593 import xapian
4594
4595 from gi.repository import Gdk, Gtk, GLib
4596-from mock import Mock
4597+from mock import Mock, patch
4598
4599 import softwarecenter.distro
4600 import softwarecenter.log
4601@@ -254,6 +254,7 @@
4602 cache = get_test_pkg_info()
4603 icons = get_test_gtk3_icon_cache()
4604 backend = get_test_install_backend()
4605+ distro = softwarecenter.distro.get_distro()
4606
4607 manager = appmanager.get_appmanager()
4608 if manager is None:
4609@@ -265,7 +266,12 @@
4610 navhistory_forward_action = Gtk.Action("navhistory_forward_action",
4611 "Forward", "Forward", None)
4612
4613- w = availablepane.AvailablePane(cache, db, 'Ubuntu', icons,
4614+ zl = "softwarecenter.backend.zeitgeist_logger.ZeitgeistLogger";
4615+ patch(zl + ".log_install_event").start().return_value = False
4616+ patch(zl + ".log_uninstall_event").start().return_value = False
4617+ patch("softwarecenter.utils.is_unity_running").start().return_value = False
4618+
4619+ w = availablepane.AvailablePane(cache, db, distro, icons,
4620 navhistory_back_action, navhistory_forward_action)
4621 w.init_view()
4622 w.show()
4623
4624=== added file 'tests/test_unity_launcher.py'
4625--- tests/test_unity_launcher.py 1970-01-01 00:00:00 +0000
4626+++ tests/test_unity_launcher.py 2013-09-17 17:52:06 +0000
4627@@ -0,0 +1,48 @@
4628+import unittest
4629+
4630+from mock import (
4631+ Mock,
4632+ patch,
4633+ )
4634+
4635+from softwarecenter.backend.unitylauncher import (
4636+ UnityLauncherInfo,
4637+ UnityLauncher,
4638+ )
4639+
4640+from tests.utils import setup_test_env
4641+setup_test_env()
4642+
4643+
4644+class TestUnityLauncherIntegration(unittest.TestCase):
4645+ """ tests the sc utils """
4646+
4647+ @patch("softwarecenter.backend.unitylauncher.UnityLauncher"
4648+ "._get_launcher_dbus_iface")
4649+ def test_apps_from_sc_agent(self, mock_dbus_iface):
4650+ mock_iface = Mock()
4651+ mock_dbus_iface.return_value = mock_iface
4652+ launcher_info = UnityLauncherInfo(
4653+ name="Meep", icon_name="icon-meep",
4654+ icon_file_path="/tmp/icon-meep.png", icon_x=15, icon_y=18,
4655+ icon_size=48, installed_desktop_file_path="software-center-agent",
4656+ trans_id="dkfjklsdf")
4657+ unity_launcher = UnityLauncher()
4658+ unity_launcher.send_application_to_launcher("meep-pkg", launcher_info)
4659+ args = mock_iface.AddLauncherItemFromPosition.call_args[0]
4660+ # basic verify
4661+ self.assertEqual(args[0], "Meep")
4662+ # ensure that the a app from software-center-agent got a temp desktop
4663+ # file
4664+ self.assertTrue(args[5].startswith("/tmp/"))
4665+ self.assertEqual(open(args[5]).read(),"""
4666+[Desktop Entry]
4667+Name=Meep
4668+Icon=/tmp/icon-meep.png
4669+Type=Application""")
4670+
4671+
4672+if __name__ == "__main__":
4673+ import logging
4674+ logging.basicConfig(level=logging.DEBUG)
4675+ unittest.main()
4676
4677=== modified file 'tests/test_utils.py'
4678--- tests/test_utils.py 2012-11-22 11:31:04 +0000
4679+++ tests/test_utils.py 2013-09-17 17:52:06 +0000
4680@@ -18,6 +18,7 @@
4681 get_test_gtk3_icon_cache,
4682 setup_test_env,
4683 UTILS_DIR,
4684+ DATA_DIR,
4685 )
4686 setup_test_env()
4687
4688@@ -35,7 +36,9 @@
4689 get_title_from_html,
4690 get_uuid,
4691 get_recommender_uuid,
4692+ get_desktop_id,
4693 is_no_display_desktop_file,
4694+ convert_desktop_file_to_installed_location,
4695 make_string_from_list,
4696 release_filename_in_lists_from_deb_line,
4697 safe_makedirs,
4698@@ -121,6 +124,16 @@
4699 d = "/usr/share/app-install/desktop/software-center:ubuntu-software-center.desktop"
4700 self.assertFalse(is_no_display_desktop_file(d))
4701
4702+ def test_get_desktop_id(self):
4703+ self.assertEqual(get_desktop_id("/usr/share/applications/ubuntu-software-center.desktop"),
4704+ "ubuntu-software-center.desktop")
4705+ self.assertEqual(get_desktop_id("/usr/share/applications/kde4-anyapp.desktop"),
4706+ "kde4-anyapp.desktop")
4707+ self.assertEqual(get_desktop_id("/usr/share/applications/kde4/anyapp.desktop"),
4708+ "kde4-anyapp.desktop")
4709+ self.assertEqual(get_desktop_id("/my/own/path/application.desktop"),
4710+ "/my/own/path/application.desktop")
4711+
4712 def test_split_icon_ext(self):
4713 for unchanged in ["foo.bar.baz", "foo.bar", "foo",
4714 "foo.pngx", "foo.png.xxx"]:
4715@@ -317,5 +330,44 @@
4716 save_person_to_config("meep")
4717 self.assertEqual(get_person_from_config(), "meep")
4718
4719+
4720+class DesktopFilepathConversionTestCase(unittest.TestCase):
4721+ def test_normal(self):
4722+ # test 'normal' case
4723+ app_install_desktop_path = os.path.join(DATA_DIR, "app-install",
4724+ "desktop", "deja-dup:deja-dup.desktop")
4725+ installed_desktop_path = convert_desktop_file_to_installed_location(
4726+ app_install_desktop_path, "deja-dup")
4727+ self.assertEqual(installed_desktop_path, os.path.join(DATA_DIR,
4728+ "applications", "deja-dup.desktop"))
4729+
4730+ def test_encoded_subdir(self):
4731+ # test encoded subdirectory case, e.g. e.g. kde4_soundkonverter.desktop
4732+ app_install_desktop_path = os.path.join(DATA_DIR, "app-install",
4733+ "desktop", "soundkonverter:kde4__soundkonverter.desktop")
4734+ installed_desktop_path = convert_desktop_file_to_installed_location(
4735+ app_install_desktop_path, "soundkonverter")
4736+ self.assertEqual(installed_desktop_path, os.path.join(DATA_DIR,
4737+ "applications", "kde4", "soundkonverter.desktop"))
4738+
4739+ def test_purchase_via_software_center_agent(self):
4740+ # test the for-purchase case (uses "software-center-agent" as its
4741+ # appdetails.desktop_file value)
4742+ # FIXME: this will only work if update-manager is installed
4743+ app_install_desktop_path = "software-center-agent"
4744+ installed_desktop_path = convert_desktop_file_to_installed_location(
4745+ app_install_desktop_path, "update-manager")
4746+ self.assertEqual(installed_desktop_path,
4747+ "/usr/share/applications/update-manager.desktop")
4748+
4749+ def test_no_value(self):
4750+ # test case where we don't have a value for app_install_desktop_path
4751+ # (e.g. for a local .deb install, see bug LP: #768158)
4752+ installed_desktop_path = convert_desktop_file_to_installed_location(
4753+ None, "update-manager")
4754+ # FIXME: this will only work if update-manager is installed
4755+ self.assertEqual(installed_desktop_path,
4756+ "/usr/share/applications/update-manager.desktop")
4757+
4758 if __name__ == "__main__":
4759 unittest.main()
4760
4761=== added file 'tests/test_zeitgeist_logger.py'
4762--- tests/test_zeitgeist_logger.py 1970-01-01 00:00:00 +0000
4763+++ tests/test_zeitgeist_logger.py 2013-09-17 17:52:06 +0000
4764@@ -0,0 +1,86 @@
4765+import unittest
4766+
4767+from mock import patch
4768+from softwarecenter.distro import get_distro
4769+from softwarecenter.backend.zeitgeist_logger import ZeitgeistLogger
4770+from tests.utils import setup_test_env
4771+
4772+setup_test_env()
4773+
4774+class TestZeitgeistLogger(unittest.TestCase):
4775+ """ tests the zeitgeist logger """
4776+
4777+ def setUp(self):
4778+ from softwarecenter.backend import zeitgeist_logger
4779+ if not zeitgeist_logger.HAVE_MODULE:
4780+ self.skipTest("Zeitgeist module missing, impossible to test")
4781+
4782+ from zeitgeist import datamodel
4783+ self.distro = get_distro()
4784+ self.logger = ZeitgeistLogger(self.distro)
4785+ self.datamodel = datamodel
4786+
4787+ def _verify_event(self, event):
4788+ self.assertEqual(event.actor, "application://" +
4789+ self.distro.get_app_id() + ".desktop")
4790+ self.assertEqual(event.manifestation,
4791+ self.datamodel.Manifestation.EVENT_MANIFESTATION.USER_ACTIVITY)
4792+
4793+ def _verify_subject(self, subject, desktop):
4794+ self.assertEqual(subject.interpretation, self.datamodel.Interpretation.SOFTWARE)
4795+ self.assertEqual(subject.manifestation, self.datamodel.Manifestation.SOFTWARE_ITEM)
4796+ self.assertEqual(subject.uri, "application://" + desktop)
4797+ self.assertEqual(subject.current_uri, subject.uri)
4798+ self.assertEqual(subject.mimetype, "application/x-desktop")
4799+
4800+ def test_construction(self):
4801+ self.assertEqual(self.logger.distro, self.distro)
4802+
4803+ @patch("zeitgeist.client.ZeitgeistClient.insert_event")
4804+ def test_log_install_event(self, mock_insert_event):
4805+ test_desktop = "software-center.desktop"
4806+ self.assertTrue(self.logger.log_install_event(test_desktop))
4807+ self.assertTrue(mock_insert_event.called)
4808+ self.assertEqual(mock_insert_event.call_count, 2)
4809+ [event] = mock_insert_event.call_args_list[0][0]
4810+ self._verify_event(event)
4811+ self.assertEqual(event.interpretation,
4812+ self.datamodel.Interpretation.EVENT_INTERPRETATION.CREATE_EVENT)
4813+ self.assertEqual(len(event.subjects), 1)
4814+ self._verify_subject(event.subjects[0], test_desktop)
4815+
4816+ [event] = mock_insert_event.call_args_list[1][0]
4817+ self._verify_event(event)
4818+ self.assertEqual(event.interpretation,
4819+ self.datamodel.Interpretation.EVENT_INTERPRETATION.ACCESS_EVENT)
4820+ self.assertEqual(len(event.subjects), 1)
4821+ self._verify_subject(event.subjects[0], test_desktop)
4822+
4823+ @patch("zeitgeist.client.ZeitgeistClient.insert_event")
4824+ def test_log_install_event_invalid_desktop(self, mock_insert_event):
4825+ self.assertFalse(self.logger.log_install_event(""))
4826+ self.assertFalse(mock_insert_event.called)
4827+
4828+ @patch("zeitgeist.client.ZeitgeistClient.insert_event")
4829+ def test_log_uninstall_event(self, mock_insert_event):
4830+ test_desktop = "software-center.desktop"
4831+ self.assertTrue(self.logger.log_uninstall_event(test_desktop))
4832+ self.assertTrue(mock_insert_event.called)
4833+ self.assertEqual(mock_insert_event.call_count, 1)
4834+ [event] = mock_insert_event.call_args_list[0][0]
4835+ self._verify_event(event)
4836+ self.assertEqual(event.interpretation,
4837+ self.datamodel.Interpretation.EVENT_INTERPRETATION.DELETE_EVENT)
4838+ self.assertEqual(len(event.subjects), 1)
4839+ self._verify_subject(event.subjects[0], test_desktop)
4840+
4841+ @patch("zeitgeist.client.ZeitgeistClient.insert_event")
4842+ def test_log_uninstall_event_invalid_desktop(self, mock_insert_event):
4843+ self.assertFalse(self.logger.log_uninstall_event(""))
4844+ self.assertFalse(mock_insert_event.called)
4845+
4846+
4847+if __name__ == "__main__":
4848+ import logging
4849+ logging.basicConfig(level=logging.DEBUG)
4850+ unittest.main()

Subscribers

People subscribed via source and target branches

to status/vote changes: