Merge lp:~dobey/software-center/update-13-10 into lp:software-center/stable-13-10
- update-13-10
- Merge into 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 |
Related bugs: |
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.
Description of the change
To post a comment you must log in.
Revision history for this message
Roberto Alsina (ralsina) : | # |
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() |
+1