diff -Nru ubuntu-sso-client-2.99.90/bin/ubuntu-sso-login-qt ubuntu-sso-client-2.99.91/bin/ubuntu-sso-login-qt --- ubuntu-sso-client-2.99.90/bin/ubuntu-sso-login-qt 2012-03-06 18:01:25.000000000 +0000 +++ ubuntu-sso-client-2.99.91/bin/ubuntu-sso-login-qt 2012-03-21 13:41:45.000000000 +0000 @@ -15,16 +15,19 @@ # You should have received a copy of the GNU General Public License along # with this program. If not, see . -"""Start the sso GTK UI.""" +"""Start the sso Qt UI.""" # Invalid name "ubuntu-sso-login-qt", pylint: disable=C0103 # Access to a protected member, pylint: disable=W0212 +import sys + from ubuntu_sso.qt.main import main from ubuntu_sso.utils.ui import parse_args -from dbus.mainloop.qt import DBusQtMainLoop -DBusQtMainLoop(set_as_default=True) +if sys.platform.startswith('linux'): + from dbus.mainloop.qt import DBusQtMainLoop + DBusQtMainLoop(set_as_default=True) if __name__ == "__main__": diff -Nru ubuntu-sso-client-2.99.90/bin/ubuntu-sso-proxy-creds-qt ubuntu-sso-client-2.99.91/bin/ubuntu-sso-proxy-creds-qt --- ubuntu-sso-client-2.99.90/bin/ubuntu-sso-proxy-creds-qt 2012-03-06 18:01:25.000000000 +0000 +++ ubuntu-sso-client-2.99.91/bin/ubuntu-sso-proxy-creds-qt 2012-03-21 13:41:45.000000000 +0000 @@ -19,9 +19,12 @@ # Invalid name, pylint: disable=C0103 -# set the dbus main loop to be used -from dbus.mainloop.qt import DBusQtMainLoop -DBusQtMainLoop(set_as_default=True) +import sys + +if sys.platform.startswith('linux'): + # set the dbus main loop to be used + from dbus.mainloop.qt import DBusQtMainLoop + DBusQtMainLoop(set_as_default=True) from ubuntu_sso.qt.proxy_dialog import main diff -Nru ubuntu-sso-client-2.99.90/data/qt/linux.qss ubuntu-sso-client-2.99.91/data/qt/linux.qss --- ubuntu-sso-client-2.99.90/data/qt/linux.qss 1970-01-01 00:00:00.000000000 +0000 +++ ubuntu-sso-client-2.99.91/data/qt/linux.qss 2012-03-21 13:41:45.000000000 +0000 @@ -0,0 +1,2 @@ +/* Styles specific to the linux platform */ + diff -Nru ubuntu-sso-client-2.99.90/data/qt/loadingoverlay.ui ubuntu-sso-client-2.99.91/data/qt/loadingoverlay.ui --- ubuntu-sso-client-2.99.90/data/qt/loadingoverlay.ui 2012-03-06 18:01:25.000000000 +0000 +++ ubuntu-sso-client-2.99.91/data/qt/loadingoverlay.ui 2012-03-21 13:41:45.000000000 +0000 @@ -52,11 +52,6 @@ 0 - - - 14 - - Getting information, please wait... diff -Nru ubuntu-sso-client-2.99.90/data/qt/proxy_credentials_dialog.ui ubuntu-sso-client-2.99.91/data/qt/proxy_credentials_dialog.ui --- ubuntu-sso-client-2.99.90/data/qt/proxy_credentials_dialog.ui 2012-03-06 18:01:25.000000000 +0000 +++ ubuntu-sso-client-2.99.91/data/qt/proxy_credentials_dialog.ui 2012-03-21 13:41:45.000000000 +0000 @@ -113,8 +113,6 @@ - 14 - 75 true diff -Nru ubuntu-sso-client-2.99.90/data/qt/reset_password.ui ubuntu-sso-client-2.99.91/data/qt/reset_password.ui --- ubuntu-sso-client-2.99.90/data/qt/reset_password.ui 2012-03-06 18:01:25.000000000 +0000 +++ ubuntu-sso-client-2.99.91/data/qt/reset_password.ui 2012-03-21 13:41:45.000000000 +0000 @@ -6,14 +6,14 @@ 0 0 - 544 - 280 + 505 + 260 - - Qt::LeftToRight + + - + 15 @@ -21,261 +21,215 @@ 0 - - - 0 - - - + + + - 15 + 3 - - - 3 - - - - - - 0 - 0 - - - - - 310 - 0 - - - - - 16777215 - 16777215 - - - - - 75 - true - - - - reset_code - - - - - - - - 0 - 0 - - - - - 300 - 0 - - - - - 300 - 16777215 - - - - - - - - - - 3 - - - - - - 0 - 0 - - - - - 310 - 0 - - - - - 16777215 - 16777215 - - - - - 75 - true - - - - password_label - - - - - - - - 0 - 0 - - - - - 300 - 0 - - - - - 300 - 16777215 - - - - QLineEdit::Password - - - - + + + + 0 + 0 + + + + + 310 + 0 + + + + + 16777215 + 16777215 + + + + + true + + + + reset_code + + - - - 3 - - - - - - 0 - 0 - - - - - 310 - 0 - - - - - 75 - true - - - - confirm_password_label - - - - - - - - 0 - 0 - - - - - 300 - 0 - - - - - 300 - 16777215 - - - - QLineEdit::Password - - - - + + + + 0 + 0 + + + + + 300 + 0 + + + + + 300 + 16777215 + + + - - + + - 0 + 3 - + - + 0 0 - 220 - 100 + 310 + 0 - 220 + 16777215 16777215 - - password_assistance + + + true + - - 20 + + password_label - - - Qt::Vertical + + + + 0 + 0 + - + - 20 - 40 + 300 + 0 - + + + 300 + 16777215 + + + + QLineEdit::Password + + + + + + + + 3 + - - - Qt::Horizontal + + + + 0 + 0 + + + + + 310 + 0 + - - QSizePolicy::Ignored + + + true + - + + confirm_password_label + + + + + + + + 0 + 0 + + + - 220 + 300 0 - + + + 300 + 16777215 + + + + QLineEdit::Password + + + + + + + 0 + 0 + + + + + 185 + 95 + + + + + 185 + 95 + + + + password_assistance + + + true + + + 20 + + + @@ -327,22 +281,5 @@ - - - confirm_password_line_edit - returnPressed() - reset_password_button - click() - - - 160 - 81 - - - 541 - 237 - - - - + diff -Nru ubuntu-sso-client-2.99.90/data/qt/resources.qrc ubuntu-sso-client-2.99.91/data/qt/resources.qrc --- ubuntu-sso-client-2.99.90/data/qt/resources.qrc 2012-03-06 18:01:25.000000000 +0000 +++ ubuntu-sso-client-2.99.91/data/qt/resources.qrc 2012-03-21 13:41:45.000000000 +0000 @@ -6,5 +6,7 @@ ../Ubuntu-B.ttf ../balloon_shape.png stylesheet.qss + windows.qss + linux.qss diff -Nru ubuntu-sso-client-2.99.90/data/qt/setup_account.ui ubuntu-sso-client-2.99.91/data/qt/setup_account.ui --- ubuntu-sso-client-2.99.90/data/qt/setup_account.ui 2012-03-06 18:01:25.000000000 +0000 +++ ubuntu-sso-client-2.99.91/data/qt/setup_account.ui 2012-03-21 13:41:45.000000000 +0000 @@ -85,7 +85,6 @@ - 75 true @@ -114,11 +113,6 @@ 16777215 - - - 11 - - false @@ -135,7 +129,6 @@ - 75 true @@ -164,11 +157,6 @@ 16777215 - - - 11 - - @@ -229,7 +217,6 @@ - 75 true @@ -258,11 +245,6 @@ 16777215 - - - 11 - - @@ -377,7 +359,6 @@ - 75 true @@ -406,13 +387,8 @@ 16777215 - - - 11 - - - Your password must be at least 8 characters long and at least contain one number and one upper later. + Your password must be at least 8 characters long and contain at least one number and one uppercase letter. @@ -477,7 +453,6 @@ - 75 true @@ -506,11 +481,6 @@ 16777215 - - - 11 - - QLineEdit::Password @@ -580,11 +550,6 @@ 16777215 - - - 11 - - diff -Nru ubuntu-sso-client-2.99.90/data/qt/ssl_dialog.ui ubuntu-sso-client-2.99.91/data/qt/ssl_dialog.ui --- ubuntu-sso-client-2.99.90/data/qt/ssl_dialog.ui 2012-03-06 18:01:25.000000000 +0000 +++ ubuntu-sso-client-2.99.91/data/qt/ssl_dialog.ui 2012-03-21 13:41:45.000000000 +0000 @@ -95,11 +95,6 @@ - - - 14 - - Do you want to connect to this server diff -Nru ubuntu-sso-client-2.99.90/data/qt/stylesheet.qss ubuntu-sso-client-2.99.91/data/qt/stylesheet.qss --- ubuntu-sso-client-2.99.90/data/qt/stylesheet.qss 2012-03-06 18:01:25.000000000 +0000 +++ ubuntu-sso-client-2.99.91/data/qt/stylesheet.qss 2012-03-21 13:41:45.000000000 +0000 @@ -1,5 +1,4 @@ QWidget { - font-family: "Ubuntu"; color: #333333; } @@ -16,7 +15,6 @@ QLabel#password_assistance { border-image: url(":/balloon_shape.png"); - font-size: 12px; } QLineEdit { @@ -91,21 +89,11 @@ min-height: 100px; } -QFrame#frm_box > QLabel { - font-size: 20px; -} - -QLabel#title_label { - font-size: 20px; -} - -QFrame#header { +WizardHeader { padding-top: 1px; padding-bottom: 1px; } QLabel#form_errors { - font: bold 14px; - color: #df2d1f; padding-bottom: 1px; } diff -Nru ubuntu-sso-client-2.99.90/data/qt/windows.qss ubuntu-sso-client-2.99.91/data/qt/windows.qss --- ubuntu-sso-client-2.99.90/data/qt/windows.qss 1970-01-01 00:00:00.000000000 +0000 +++ ubuntu-sso-client-2.99.91/data/qt/windows.qss 2012-03-21 13:41:45.000000000 +0000 @@ -0,0 +1,5 @@ +/* Styles specific to the windows platform */ + +QWidget { + font-family: "Ubuntu"; +} diff -Nru ubuntu-sso-client-2.99.90/debian/changelog ubuntu-sso-client-2.99.91/debian/changelog --- ubuntu-sso-client-2.99.90/debian/changelog 2012-03-07 19:17:50.000000000 +0000 +++ ubuntu-sso-client-2.99.91/debian/changelog 2012-03-21 15:38:13.000000000 +0000 @@ -1,3 +1,71 @@ +ubuntu-sso-client (2.99.91-0ubuntu1) precise; urgency=low + + * New upstream release: + - Do not allow ssl errors to be ignored (LP: #959390). + - Handle wrong credentials properly in qtnetwork webclient + (LP: #957317). + - Use HTTPClientFactory to allow replacing the reactor used to connect + (LP: #929207). + - Decode the content of help_text (LP: #951371). + - Adding missing file for translation (LP: #951376). + - Adding a general error message when the argument received by + build_general_error_message is not a dict (LP: #865176). + - Adding some checks to setup_page (LP: #951461). + - Adding a padding to the right margin of the reset layout, and align + one of its layout to the left (LP: #945065). + - Executing hide_error when the user click the refresh captcha link, + not inside of the _refresh_captcha method, because this is executed + automatically when a captcha error is generated, so we will always + miss the error message (LP: #955010). + - Removing the align property from the label that wasn't necessary + and was breakint the work wrap. Also adjust the height of the widget + depending if it has more than one line (LP: #940392). + - Improve logging operations (LP: #934500). + - Making LINK_STYLE to be unicode (LP: #950953). + - Setting the window title equal to the app_name (LP: #949744). + - The _move_to_email_verification_page wasn't receiving the params + that the signal emits (LP: #945066). + - Improve the grammar for the CLOSE_AND_SETUP_LATER button text + (LP: #949978). + - Changed the way in which we deal with proxies to work around bugs + found in the QNetworkAccessManager (LP: #957313). + - Stopped listening to the proxyAuthenticationRequired to avoid the + dialog showing more than once (LP: #957170) + - Made changes in the way the webclient is selected to ensure that qt + is used when possible (LP: #957169). + - Connected the WebKit browser correctly so that the tc page gets + loaded (LP: #933081). + - Added code to check if the browser with the t&c was already loaded. + If it is just show the t&c page, otherwise perform the request + (LP: #956185). + - Added a translatable string to give more context of the ssl cert +   info to the user (LP: #948119). + - Provided the logic required for the Qt webclient implementation to + detect ssl errors and spawn the ssl dialog to allow the user accept + the ssl cert exceptions (LP: #948134). + - Changed the qt webclient implementation to use a proxy factory so + that the correct proxy is chosen according to the request + (LP: #950088). + - Added the required code to allow the webclient use authenticated + proxies and request the creds when needed (LP: #933727). + - The tooltip should not be shown for titles and subtitles when + no cut off was needed (LP: #949741). + - Enable platform-specific styling (LP: #953318). + - Only import DBus on Linux (LP: #956304). + - Don't hard-code font sizes. + - Remove usage of weight property as a numeric; just use bold + property instead (LP: #953062). + * Removed patches which were included upstream. + * debian/watch: + - Updated url to fetch tarball from latest milestone. + * debian/ubuntu-sso-client-qt.install: + - Install Qt UI executables for providing proxy authentication support. + * debian/control: + - Added new dependency on python-openssl to python-ubuntu-sso-client. + - Improved description for ubuntu-sso-client-qt binary package. + + -- Natalia Bidart (nessita) Wed, 21 Mar 2012 12:37:28 -0300 + ubuntu-sso-client (2.99.90-0ubuntu2) precise; urgency=low * debian/patches/fix-945066.patch: diff -Nru ubuntu-sso-client-2.99.90/debian/control ubuntu-sso-client-2.99.91/debian/control --- ubuntu-sso-client-2.99.90/debian/control 2012-03-07 18:30:43.000000000 +0000 +++ ubuntu-sso-client-2.99.91/debian/control 2012-03-21 15:37:11.000000000 +0000 @@ -38,6 +38,7 @@ python-dbus, python-httplib2 (>= 0.7.2), python-oauth, + python-openssl, python-simplejson, python-twisted-core, python-twisted-web, @@ -78,7 +79,8 @@ Provides: ubuntu-sso-client-gui Description: Ubuntu Single Sign-On client - Qt frontend Qt frontend to be used by the desktop service to sign into Ubuntu services - via SSO. + via SSO. This package also provides the Qt frontend to handle proxy + authentication. Package: python-ubuntu-sso-client.tests Architecture: all @@ -93,4 +95,3 @@ python-zope.interface Description: Ubuntu Single Sign-On client - Test suite Test suite for the ubuntu-sso-client library. - diff -Nru ubuntu-sso-client-2.99.90/debian/patches/fix-945066.patch ubuntu-sso-client-2.99.91/debian/patches/fix-945066.patch --- ubuntu-sso-client-2.99.90/debian/patches/fix-945066.patch 2012-03-07 18:27:00.000000000 +0000 +++ ubuntu-sso-client-2.99.91/debian/patches/fix-945066.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,141 +0,0 @@ -=== modified file 'ubuntu_sso/qt/current_user_sign_in_page.py' ---- old/ubuntu_sso/qt/current_user_sign_in_page.py 2012-03-05 21:21:20 +0000 -+++ new/ubuntu_sso/qt/current_user_sign_in_page.py 2012-03-05 21:21:20 +0000 -@@ -61,6 +61,11 @@ - } - return result - -+ @property -+ def password(self): -+ """Return the content of the password edit.""" -+ return unicode(self.ui.password_edit.text()) -+ - def on_user_not_validated(self, app_name, email): - """Show the validate email page.""" - self.hide_overlay() - -=== modified file 'ubuntu_sso/qt/setup_account_page.py' ---- old/ubuntu_sso/qt/setup_account_page.py 2012-03-05 21:21:20 +0000 -+++ new/ubuntu_sso/qt/setup_account_page.py 2012-03-05 21:21:20 +0000 -@@ -112,6 +112,11 @@ - } - return result - -+ @property -+ def password(self): -+ """Return the content of the password edit.""" -+ return unicode(self.ui.password_edit.text()) -+ - # Invalid name "initializePage" - # pylint: disable=C0103 - - -=== modified file 'ubuntu_sso/qt/tests/test_current_user_sign_in_page.py' ---- old/ubuntu_sso/qt/tests/test_current_user_sign_in_page.py 2012-03-05 21:21:20 +0000 -+++ new/ubuntu_sso/qt/tests/test_current_user_sign_in_page.py 2012-03-05 21:21:20 +0000 -@@ -24,6 +24,7 @@ - FakePageUiStyle, - FakeWizardButtonStyle, - ) -+from ubuntu_sso.tests import EMAIL - - - # pylint: disable=W0212 -@@ -139,19 +140,16 @@ - - def test_on_logged_in(self): - """Test the on_login_in method.""" -- email = 'email@example' -- self.ui.ui.email_edit.setText(email) -- -- self.assert_signal_emitted(self.ui.userLoggedIn, (email,), -- self.ui.on_logged_in, self.app_name, email) -+ self.ui.ui.email_edit.setText(EMAIL) -+ self.assert_signal_emitted(self.ui.userLoggedIn, (EMAIL,), -+ self.ui.on_logged_in, self.app_name, EMAIL) - self.assertTrue(self.ui.isEnabled()) - - def test_on_user_not_validated(self): - """Test the navigation flow on user not validated.""" -- email = 'email@example' -- self.ui.ui.email_edit.setText(email) -- self.assert_signal_emitted(self.ui.userNotValidated, (email,), -- self.ui.on_user_not_validated, self.app_name, email) -+ self.ui.ui.email_edit.setText(EMAIL) -+ self.assert_signal_emitted(self.ui.userNotValidated, (EMAIL,), -+ self.ui.on_user_not_validated, self.app_name, EMAIL) - - def test_on_forgotten_password(self): - """Test the on_forgotten_password method.""" - -=== modified file 'ubuntu_sso/qt/tests/test_setup_account.py' ---- old/ubuntu_sso/qt/tests/test_setup_account.py 2012-03-05 21:21:20 +0000 -+++ new/ubuntu_sso/qt/tests/test_setup_account.py 2012-03-05 21:21:20 +0000 -@@ -174,7 +174,6 @@ - """Test on_user_registered method.""" - email = 'email@example' - self.ui.ui.email_edit.setText(email) -- - self.assert_signal_emitted(self.ui.userRegistered, (email,), - self.ui.on_user_registered, self.app_name, email) - - -=== modified file 'ubuntu_sso/qt/tests/test_ubuntu_sso_wizard.py' ---- old/ubuntu_sso/qt/tests/test_ubuntu_sso_wizard.py 2012-03-05 14:37:43 +0000 -+++ new/ubuntu_sso/qt/tests/test_ubuntu_sso_wizard.py 2012-03-05 21:21:20 +0000 -@@ -19,6 +19,11 @@ - from PyQt4 import QtGui - from twisted.internet import defer - -+from ubuntu_sso.tests import ( -+ APP_NAME, -+ EMAIL, -+ PASSWORD, -+) - from ubuntu_sso.qt import PREFERED_UI_SIZE, ubuntu_sso_wizard - from ubuntu_sso.qt.tests import ( - BaseTestCase, -@@ -146,3 +151,25 @@ - self.ui.reset_password.passwordChanged.emit('') - expected = ((self.ui.current_user,), {}) - self.assertEqual(expected, self._called) -+ -+ def test_email_verification_page_params_from_current_user(self): -+ """Tests that email_verification_page receives the proper params.""" -+ self.ui.show() -+ self.addCleanup(self.ui.hide) -+ self.ui._next_id = self.ui.current_user_page_id -+ self.ui.next() -+ self.ui.current_user.ui.email_edit.setText(EMAIL) -+ self.ui.current_user.ui.password_edit.setText(PASSWORD) -+ self.ui.current_user.on_user_not_validated(APP_NAME, EMAIL) -+ self.assertEqual(EMAIL, self.ui.email_verification.email) -+ self.assertEqual(PASSWORD, self.ui.email_verification.password) -+ -+ def test_email_verification_page_params_from_setup(self): -+ """Tests that email_verification_page receives the proper params.""" -+ self.ui.show() -+ self.addCleanup(self.ui.hide) -+ self.ui.setup_account.ui.email_edit.setText(EMAIL) -+ self.ui.setup_account.ui.password_edit.setText(PASSWORD) -+ self.ui.setup_account.on_user_registered(APP_NAME, {}) -+ self.assertEqual(EMAIL, self.ui.email_verification.email) -+ self.assertEqual(PASSWORD, self.ui.email_verification.password) - -=== modified file 'ubuntu_sso/qt/ubuntu_sso_wizard.py' ---- old/ubuntu_sso/qt/ubuntu_sso_wizard.py 2012-03-05 21:21:20 +0000 -+++ new/ubuntu_sso/qt/ubuntu_sso_wizard.py 2012-03-05 21:21:20 +0000 -@@ -154,9 +154,11 @@ - self.next() - self._next_id = -1 - -- def _move_to_email_verification_page(self): -+ def _move_to_email_verification_page(self, email): - """Move to the email verification page wizard.""" - self._next_id = self.email_verification_page_id -+ self.email_verification.email = unicode(email) -+ self.email_verification.password = self.currentPage().password - self.next() - self._next_id = -1 - - diff -Nru ubuntu-sso-client-2.99.90/debian/patches/series ubuntu-sso-client-2.99.91/debian/patches/series --- ubuntu-sso-client-2.99.90/debian/patches/series 2012-03-07 18:32:14.000000000 +0000 +++ ubuntu-sso-client-2.99.91/debian/patches/series 1970-01-01 00:00:00.000000000 +0000 @@ -1 +0,0 @@ -fix-945066.patch diff -Nru ubuntu-sso-client-2.99.90/debian/ubuntu-sso-client-qt.install ubuntu-sso-client-2.99.91/debian/ubuntu-sso-client-qt.install --- ubuntu-sso-client-2.99.90/debian/ubuntu-sso-client-qt.install 2012-03-07 18:30:43.000000000 +0000 +++ ubuntu-sso-client-2.99.91/debian/ubuntu-sso-client-qt.install 2012-03-21 14:47:32.000000000 +0000 @@ -1,3 +1,5 @@ debian/tmp/usr/share/ubuntu-sso-client/qt debian/tmp/usr/lib/ubuntu-sso-client/ubuntu-sso-login-qt +debian/tmp/usr/lib/ubuntu-sso-client/ubuntu-sso-proxy-creds-qt +debian/tmp/usr/lib/ubuntu-sso-client/ubuntu-sso-ssl-certificate-qt debian/tmp/usr/lib/python2.*/*-packages/*/ubuntu_sso/qt diff -Nru ubuntu-sso-client-2.99.90/debian/watch ubuntu-sso-client-2.99.91/debian/watch --- ubuntu-sso-client-2.99.90/debian/watch 2012-03-07 18:30:43.000000000 +0000 +++ ubuntu-sso-client-2.99.91/debian/watch 2012-03-21 14:13:05.000000000 +0000 @@ -1,2 +1,2 @@ version=3 -http://launchpad.net/ubuntu-sso-client/stable-3-0/2.99.90/ .*/ubuntu-sso-client-([0-9.]+)\.tar\.gz +http://launchpad.net/ubuntu-sso-client/stable-3-0/2.99.91/ .*/ubuntu-sso-client-([0-9.]+)\.tar\.gz diff -Nru ubuntu-sso-client-2.99.90/PKG-INFO ubuntu-sso-client-2.99.91/PKG-INFO --- ubuntu-sso-client-2.99.90/PKG-INFO 2012-03-06 18:01:59.000000000 +0000 +++ ubuntu-sso-client-2.99.91/PKG-INFO 2012-03-21 13:43:46.000000000 +0000 @@ -1,6 +1,6 @@ Metadata-Version: 1.1 Name: ubuntu-sso-client -Version: 2.99.90 +Version: 2.99.91 Summary: Ubuntu Single Sign-On client Home-page: https://launchpad.net/ubuntu-sso-client Author: Natalia Bidart diff -Nru ubuntu-sso-client-2.99.90/po/POTFILES.in ubuntu-sso-client-2.99.91/po/POTFILES.in --- ubuntu-sso-client-2.99.90/po/POTFILES.in 2012-03-06 18:01:25.000000000 +0000 +++ ubuntu-sso-client-2.99.91/po/POTFILES.in 2012-03-21 13:41:45.000000000 +0000 @@ -1 +1,2 @@ ubuntu_sso/gtk/gui.py +ubuntu_sso/utils/ui.py diff -Nru ubuntu-sso-client-2.99.90/run-tests ubuntu-sso-client-2.99.91/run-tests --- ubuntu-sso-client-2.99.90/run-tests 2012-03-06 18:01:25.000000000 +0000 +++ ubuntu-sso-client-2.99.91/run-tests 2012-03-21 13:41:45.000000000 +0000 @@ -57,7 +57,7 @@ echo "*** Running QT test suite for ""$MODULE"" ***" ./setup.py build -USE_QT_MAINLOOP=True $XVFB_CMDLINE u1trial --reactor=qt4 --gui -p "$GTK_TESTS_PATH" -i "test_windows.py" "$MODULE" +$XVFB_CMDLINE u1trial --reactor=qt4 --gui -p "$GTK_TESTS_PATH" -i "test_windows.py" "$MODULE" rm -rf _trial_temp rm -rf build diff -Nru ubuntu-sso-client-2.99.90/setup.py ubuntu-sso-client-2.99.91/setup.py --- ubuntu-sso-client-2.99.90/setup.py 2012-03-06 18:01:26.000000000 +0000 +++ ubuntu-sso-client-2.99.91/setup.py 2012-03-21 13:41:46.000000000 +0000 @@ -334,7 +334,7 @@ DistUtilsExtra.auto.setup( name='ubuntu-sso-client', - version='2.99.90', + version='2.99.91', license='GPL v3', author='Natalia Bidart', author_email='natalia.bidart@canonical.com', diff -Nru ubuntu-sso-client-2.99.90/ubuntu_sso/gtk/gui.py ubuntu-sso-client-2.99.91/ubuntu_sso/gtk/gui.py --- ubuntu-sso-client-2.99.90/ubuntu_sso/gtk/gui.py 2012-03-06 18:01:25.000000000 +0000 +++ ubuntu-sso-client-2.99.91/ubuntu_sso/gtk/gui.py 2012-03-21 13:41:45.000000000 +0000 @@ -90,23 +90,6 @@ return c # pylint: enable=C0103 - -# To be removed when Python bindings provide these constants -# as per http://code.google.com/p/pywebkitgtk/issues/detail?id=44 -# WebKitLoadStatus -WEBKIT_LOAD_PROVISIONAL = 0 -WEBKIT_LOAD_COMMITTED = 1 -WEBKIT_LOAD_FINISHED = 2 -WEBKIT_LOAD_FIRST_VISUALLY_NON_EMPTY_LAYOUT = 3 -WEBKIT_LOAD_FAILED = 4 -# WebKitWebNavigationReason -WEBKIT_WEB_NAVIGATION_REASON_LINK_CLICKED = 0 -WEBKIT_WEB_NAVIGATION_REASON_FORM_SUBMITTED = 1 -WEBKIT_WEB_NAVIGATION_REASON_BACK_FORWARD = 2 -WEBKIT_WEB_NAVIGATION_REASON_RELOAD = 3 -WEBKIT_WEB_NAVIGATION_REASON_FORM_RESUBMITTED = 4 -WEBKIT_WEB_NAVIGATION_REASON_OTHER = 5 - DEFAULT_WIDTH = 30 # To be replaced by values from the theme (LP: #616526) HELP_TEXT_COLOR = parse_color("#bfbfbf") @@ -947,23 +930,14 @@ self._set_current_page(self.processing_vbox) - def on_tc_button_clicked(self, *args, **kwargs): - """The T&C button was clicked, create the browser and load terms.""" + def _add_webkit_browser(self): + """Add the webkit browser for the t&c.""" # delay the import of webkit to be able to build without it from gi.repository import WebKit # pylint: disable=E0611 + browser = WebKit.WebView() - # The signal WebKitWebView::load-finished is deprecated and should not - # be used in newly-written code. Use the "load-status" property - # instead. Connect to "notify::load-status" to monitor loading. - - # nataliabidart (2010-10-04): connecting this signal makes the loading - # of the Ubuntu One terms URL to fail. So we're using the deprecated - # 'load-finished' for now. - - #browser.connect('notify::load-status', - # self.on_tc_browser_notify_load_status) - browser.connect('load-finished', + browser.connect('notify::load-status', self.on_tc_browser_notify_load_status) browser.connect('navigation-policy-decision-requested', self.on_tc_browser_navigation_requested) @@ -978,7 +952,14 @@ browser.load_uri(self.tc_url) browser.show() self.tc_browser_window.add(browser) - self._set_current_page(self.processing_vbox) + + def on_tc_button_clicked(self, *args, **kwargs): + """The T&C button was clicked, create the browser and load terms.""" + if self.tc_browser_window.get_child() is None: + self._add_webkit_browser() + self._set_current_page(self.processing_vbox) + else: + self._set_current_page(self.tc_browser_vbox) def on_tc_back_button_clicked(self, *args, **kwargs): """T & C 'back' button was clicked, return to the previous page.""" @@ -986,14 +967,18 @@ def on_tc_browser_notify_load_status(self, browser, *args, **kwargs): """The T&C page is being loaded.""" - if browser.get_load_status() == WEBKIT_LOAD_FINISHED: + from gi.repository import WebKit # pylint: disable=E0611 + + if browser.get_load_status().real == WebKit.LoadStatus.FINISHED: self._set_current_page(self.tc_browser_vbox) def on_tc_browser_navigation_requested(self, browser, frame, request, action, decision, *args, **kwargs): """The user wants to navigate within the T&C browser.""" + from gi.repository import WebKit # pylint: disable=E0611 + if action is not None and \ - action.get_reason() == WEBKIT_WEB_NAVIGATION_REASON_LINK_CLICKED: + action.get_reason() == WebKit.WebNavigationReason.LINK_CLICKED: if decision is not None: decision.ignore() url = action.get_original_uri() diff -Nru ubuntu-sso-client-2.99.90/ubuntu_sso/gtk/tests/test_gui.py ubuntu-sso-client-2.99.91/ubuntu_sso/gtk/tests/test_gui.py --- ubuntu-sso-client-2.99.90/ubuntu_sso/gtk/tests/test_gui.py 2012-03-06 18:01:25.000000000 +0000 +++ ubuntu-sso-client-2.99.91/ubuntu_sso/gtk/tests/test_gui.py 2012-03-21 13:41:45.000000000 +0000 @@ -134,7 +134,7 @@ def get_load_status(self): """Return the current load status.""" - return gui.WEBKIT_LOAD_FINISHED + return WebKit.LoadStatus.FINISHED def show(self): """Show this instance.""" @@ -981,7 +981,7 @@ def test_notify_load_finished_connected(self): """The 'load-finished' signal is connected.""" expected = [self.ui.on_tc_browser_notify_load_status] - self.assertEqual(self.browser._signals['load-finished'], + self.assertEqual(self.browser._signals['notify::load-status'], expected) def test_tc_loaded_morphs_into_tc_browser_vbox(self): @@ -998,7 +998,7 @@ def test_navigation_requested_succeeds_for_no_clicking(self): """The navigation request succeeds when user hasn't clicked a link.""" action = WebKit.WebNavigationAction() - action.set_reason(gui.WEBKIT_WEB_NAVIGATION_REASON_OTHER) + action.set_reason(WebKit.WebNavigationReason.OTHER) decision = WebKit.WebPolicyDecision() decision.use = self._set_called @@ -1011,7 +1011,7 @@ def test_navigation_requested_ignores_clicked_links(self): """The navigation request is ignored if a link was clicked.""" action = WebKit.WebNavigationAction() - action.set_reason(gui.WEBKIT_WEB_NAVIGATION_REASON_LINK_CLICKED) + action.set_reason(WebKit.WebNavigationReason.LINK_CLICKED) decision = WebKit.WebPolicyDecision() decision.ignore = self._set_called @@ -1037,7 +1037,7 @@ """ url = 'http://something.com/yadda' action = WebKit.WebNavigationAction() - action.set_reason(gui.WEBKIT_WEB_NAVIGATION_REASON_LINK_CLICKED) + action.set_reason(WebKit.WebNavigationReason.LINK_CLICKED) action.set_original_uri(url) decision = WebKit.WebPolicyDecision() @@ -1050,6 +1050,35 @@ self.ui.on_tc_browser_navigation_requested(**kwargs) self.assertEqual(self._called, ((url,), {})) + def test_on_tc_button_clicked_no_child(self): + """Test the tc loading with no child.""" + called = [] + + def fake_add_browser(): + """Fake add browser.""" + called.append('fake_add_browser') + + self.patch(self.ui, '_add_webkit_browser', fake_add_browser) + self.patch(self.ui.tc_browser_window, 'get_child', lambda: None) + + self.ui.on_tc_button_clicked() + self.assertIn('fake_add_browser', called) + + def test_on_tc_button_clicked_child(self): + """Test the tc loading with child.""" + called = [] + + def fake_add_browser(i_self): + """Fake add browser.""" + called.append('fake_add_browser') + + self.patch(self.ui, '_add_webkit_browser', fake_add_browser) + + browser = WebKit.WebView() + self.ui.tc_browser_window.add(browser) + self.ui.on_tc_button_clicked() + self.assertNotIn('fake_add_browser', called) + class RegistrationErrorTestCase(UbuntuSSOClientTestCase): """Test suite for the user registration error handling.""" diff -Nru ubuntu-sso-client-2.99.90/ubuntu_sso/__init__.py ubuntu-sso-client-2.99.91/ubuntu_sso/__init__.py --- ubuntu-sso-client-2.99.90/ubuntu_sso/__init__.py 2012-03-06 18:01:25.000000000 +0000 +++ ubuntu-sso-client-2.99.91/ubuntu_sso/__init__.py 2012-03-21 13:41:45.000000000 +0000 @@ -15,6 +15,8 @@ # with this program. If not, see . """Ubuntu Single Sign On client code.""" +import sys + # DBus constants DBUS_BUS_NAME = "com.ubuntu.sso" @@ -29,7 +31,13 @@ # return codes for UIs USER_SUCCESS = 0 USER_CANCELLATION = 10 +EXCEPTION_RAISED = 11 # available UIs UI_EXECUTABLE_GTK = 'ubuntu-sso-login-gtk' UI_EXECUTABLE_QT = 'ubuntu-sso-login-qt' +UI_PROXY_CREDS_DIALOG = 'ubuntu-sso-proxy-creds-qt' + +if getattr(sys, "frozen", None) is not None and sys.platform == "win32": + UI_EXECUTABLE_QT += ".exe" + UI_PROXY_CREDS_DIALOG += ".exe" diff -Nru ubuntu-sso-client-2.99.90/ubuntu_sso/qt/common.py ubuntu-sso-client-2.99.91/ubuntu_sso/qt/common.py --- ubuntu-sso-client-2.99.90/ubuntu_sso/qt/common.py 2012-03-06 18:01:25.000000000 +0000 +++ ubuntu-sso-client-2.99.91/ubuntu_sso/qt/common.py 2012-03-21 13:41:45.000000000 +0000 @@ -28,9 +28,9 @@ ) # all the text + styles that are used in the gui -BAD = u' %s ' -GOOD = u' %s ' -NORMAL = u' %s ' +BAD = u' %s ' +GOOD = u' %s ' +NORMAL = u' %s ' def password_assistance(line_edit, assistance, icon_type=BAD): diff -Nru ubuntu-sso-client-2.99.90/ubuntu_sso/qt/current_user_sign_in_page.py ubuntu-sso-client-2.99.91/ubuntu_sso/qt/current_user_sign_in_page.py --- ubuntu-sso-client-2.99.90/ubuntu_sso/qt/current_user_sign_in_page.py 2012-03-06 18:01:25.000000000 +0000 +++ ubuntu-sso-client-2.99.91/ubuntu_sso/qt/current_user_sign_in_page.py 2012-03-21 13:41:45.000000000 +0000 @@ -61,6 +61,11 @@ } return result + @property + def password(self): + """Return the content of the password edit.""" + return unicode(self.ui.password_edit.text()) + def on_user_not_validated(self, app_name, email): """Show the validate email page.""" self.hide_overlay() @@ -76,6 +81,7 @@ def initializePage(self): """Setup UI details.""" + logger.debug('initializePage - About to show CurrentUserSignInPage') self.setButtonText(QtGui.QWizard.CancelButton, CANCEL_BUTTON) # Layout without custom button 1, # without finish button @@ -94,10 +100,8 @@ def _set_translated_strings(self): """Set the translated strings.""" - logger.debug('CurrentUserSignInPage._set_translated_strings') self.setTitle(LOGIN_TITLE.format(app_name=self.app_name)) self.setSubTitle(LOGIN_SUBTITLE % {'app_name': self.app_name}) - self.ui.email_label.setText(EMAIL_LABEL) self.ui.password_label.setText(LOGIN_PASSWORD_LABEL) forgotten_text = LINK_STYLE.format(link_url='#', @@ -107,7 +111,6 @@ def _connect_ui(self): """Connect the buttons to perform actions.""" - logger.debug('CurrentUserSignInPage._connect_buttons') self.ui.forgot_password_label.linkActivated.connect( self.on_forgotten_password) self.ui.email_edit.textChanged.connect(self._validate) @@ -116,18 +119,16 @@ def _validate(self): """Perform input validation.""" - valid = True correct_mail = is_correct_email(unicode(self.ui.email_edit.text())) - password = unicode(self.ui.password_edit.text()) - if not correct_mail or not password: - valid = False - self.ui.sign_in_button.setEnabled(valid) + correct_password = len(unicode(self.ui.password_edit.text())) > 0 + enabled = correct_mail and correct_password + self.ui.sign_in_button.setEnabled(enabled) def login(self): """Perform the login using the self.backend.""" - logger.debug('CurrentUserSignInPage.login') # grab the data from the view and call the backend email = unicode(self.ui.email_edit.text()) + logger.info('CurrentUserSignInPage.login for: %s', email) password = unicode(self.ui.password_edit.text()) args = (self.app_name, email, password) if self.ping_url: @@ -146,18 +147,18 @@ # let the user know logger.error('Got error when login %s, error: %s', self.app_name, error) - self.show_error(self.app_name, build_general_error_message(error)) + self.show_error(build_general_error_message(error)) def on_logged_in(self, app_name, result): """We managed to log in.""" logger.info('Logged in for %s', app_name) self.hide_overlay() email = unicode(self.ui.email_edit.text()) + logger.debug('About to emit userLoggedIn signal with: (%s).', email) self.userLoggedIn.emit(email) - logger.debug('Wizard.loginSuccess emitted.') def on_forgotten_password(self, link=None): """Show the user the forgotten password page.""" - logger.info('Forgotten password') self.hide_overlay() + logger.debug('About to emit passwordForgotten signal') self.passwordForgotten.emit() diff -Nru ubuntu-sso-client-2.99.90/ubuntu_sso/qt/email_verification_page.py ubuntu-sso-client-2.99.91/ubuntu_sso/qt/email_verification_page.py --- ubuntu-sso-client-2.99.90/ubuntu_sso/qt/email_verification_page.py 2012-03-06 18:01:25.000000000 +0000 +++ ubuntu-sso-client-2.99.91/ubuntu_sso/qt/email_verification_page.py 2012-03-21 13:41:45.000000000 +0000 @@ -71,7 +71,6 @@ def _connect_ui(self): """Set the connection of signals.""" - logger.debug('EmailVerificationController._connect_ui') self.ui.verification_code_edit.textChanged.connect( self.validate_form) self.next_button.clicked.connect(self.validate_email) @@ -84,7 +83,6 @@ def _set_translated_strings(self): """Set the different titles.""" - logger.debug('EmailVerificationController._set_titles') self.header.set_title(VERIFY_EMAIL_TITLE) self.header.set_subtitle(VERIFY_EMAIL_CONTENT % { "app_name": self.app_name, @@ -103,7 +101,8 @@ def validate_email(self): """Call the next action.""" - logger.debug('EmailVerificationController.validate_email') + logger.debug('EmailVerificationController.validate_email for: %s', + self.email) code = unicode(self.ui.verification_code_edit.text()) args = (self.app_name, self.email, self.password, code) self.hide_error() @@ -123,21 +122,25 @@ def on_email_validated(self, app_name, email): """Signal thrown after the email is validated.""" - logger.info('EmailVerificationController.on_email_validated') + logger.info('EmailVerificationController.on_email_validated for %s, ' + 'email: %s', app_name, email) self.hide_overlay() self.registrationSuccess.emit(self.email) def on_email_validation_error(self, app_name, error): """Signal thrown when there's a problem validating the email.""" + logger.error('Got error on email validation %s, error: %s', + app_name, error) self.hide_overlay() msg = error.pop(ERROR_EMAIL_TOKEN, '') msg += build_general_error_message(error) - self.show_error(self.app_name, msg) + self.show_error(msg) # pylint: disable=C0103 def initializePage(self): """Called to prepare the page just before it is shown.""" + logger.debug('initializePage - About to show EmailVerificationPage') self.next_button.setDefault(True) self.next_button.setEnabled(False) self.wizard().setButtonLayout([QtGui.QWizard.Stretch]) diff -Nru ubuntu-sso-client-2.99.90/ubuntu_sso/qt/enhanced_check_box.py ubuntu-sso-client-2.99.91/ubuntu_sso/qt/enhanced_check_box.py --- ubuntu-sso-client-2.99.90/ubuntu_sso/qt/enhanced_check_box.py 2012-03-06 18:01:25.000000000 +0000 +++ ubuntu-sso-client-2.99.91/ubuntu_sso/qt/enhanced_check_box.py 2012-03-21 13:41:45.000000000 +0000 @@ -24,11 +24,12 @@ class EnhancedCheckBox(QtGui.QCheckBox): """Enhanced QCheckBox to support links in the message displayed.""" - def __init__(self, text=""): - QtGui.QCheckBox.__init__(self) + def __init__(self, text="", parent=None): + QtGui.QCheckBox.__init__(self, parent) hbox = QtGui.QHBoxLayout() + hbox.setAlignment(QtCore.Qt.AlignLeft) self.text_label = QtGui.QLabel(text) - self.text_label.setAlignment(QtCore.Qt.AlignTop) + self.text_label.setWordWrap(True) self.text_label.setOpenExternalLinks(True) padding = self.iconSize().width() self.text_label.setStyleSheet("margin-top: -3px;" @@ -37,6 +38,11 @@ hbox.addWidget(self.text_label) self.setLayout(hbox) + if parent is not None: + lines = self.text_label.width() / float(parent.width()) + self.text_label.setMinimumWidth(parent.width()) + self.setMinimumHeight(self.height() * lines) + self.stateChanged.connect(self.text_label.setFocus) def text(self): diff -Nru ubuntu-sso-client-2.99.90/ubuntu_sso/qt/forgotten_password_page.py ubuntu-sso-client-2.99.91/ubuntu_sso/qt/forgotten_password_page.py --- ubuntu-sso-client-2.99.90/ubuntu_sso/qt/forgotten_password_page.py 2012-03-06 18:01:25.000000000 +0000 +++ ubuntu-sso-client-2.99.91/ubuntu_sso/qt/forgotten_password_page.py 2012-03-21 13:41:45.000000000 +0000 @@ -63,6 +63,7 @@ def initializePage(self): """Set the initial state of ForgottenPassword page.""" + logger.debug('initializePage - About to show ForgottenPasswordPage') self.ui.send_button.setDefault(True) enabled = not self.ui.email_line_edit.text().isEmpty() self.ui.send_button.setEnabled(enabled) @@ -98,6 +99,7 @@ """Send the request password operation.""" self.hide_error() args = (self.app_name, self.email_address) + logger.debug('Sending request new password for %s, email: %s', *args) f = self.backend.request_password_reset_token error_handler = partial(self._handle_error, f, @@ -113,6 +115,8 @@ def on_password_reset_token_sent(self, app_name, email): """Action taken when we managed to get the password reset done.""" + logger.info('ForgottenPasswordPage.on_password_reset_token_sent for ' + '%s, email: %s', app_name, email) # ignore the result and move to the reset page self.hide_overlay() self.passwordResetTokenSent.emit(email) @@ -123,4 +127,4 @@ # set the error message self.hide_overlay() msg = REQUEST_PASSWORD_TOKEN_WRONG_EMAIL - self.show_error(self.app_name, msg) + self.show_error(msg) diff -Nru ubuntu-sso-client-2.99.90/ubuntu_sso/qt/__init__.py ubuntu-sso-client-2.99.91/ubuntu_sso/qt/__init__.py --- ubuntu-sso-client-2.99.90/ubuntu_sso/qt/__init__.py 2012-03-06 18:01:25.000000000 +0000 +++ ubuntu-sso-client-2.99.91/ubuntu_sso/qt/__init__.py 2012-03-21 13:41:45.000000000 +0000 @@ -18,19 +18,27 @@ import collections +from PyQt4 import QtGui, QtCore -LINK_STYLE = ('' +from ubuntu_sso.logger import setup_gui_logging +from ubuntu_sso.utils.ui import GENERIC_BACKEND_ERROR + +logger = setup_gui_logging('ubuntu_sso.qt') + +LINK_STYLE = (u'' '{link_text}') ERROR_ALL = '__all__' -ERROR_STYLE = u'%s' +ERROR_STYLE = u'%s' ERROR_MESSAGE = 'message' PREFERED_UI_SIZE = {'width': 550, 'height': 525} -TITLE_STYLE = u'%s' +TITLE_STYLE = u'%s' +WINDOW_TITLE = 'Ubuntu Single Sign On' # Based on the gtk implementation def build_general_error_message(errordict): """Build a user-friendly error message from the errordict.""" + logger.debug('build_general_error_message: errordict is: %r.', errordict) result = '' if isinstance(errordict, collections.Mapping): msg1 = errordict.get(ERROR_ALL) @@ -50,5 +58,25 @@ result = '\n'.join( [('%s: %s' % (k, v)) for k, v in errordict.iteritems()]) else: - result = repr(errordict) + result = GENERIC_BACKEND_ERROR + logger.error('build_general_error_message with unknown error: %r', + errordict) + + logger.info('build_general_error_message: returning %r.', result) return result + + +def maybe_elide_text(label, text, width, markup=None): + """Set 'text' to be the 'label's text. + + If 'text' is longer than 'width', set the label's tooltip to be the full + text, and the text itself to be the elided version of 'text'. + + """ + fm = QtGui.QFontMetrics(label.font()) + elided_text = fm.elidedText(text, QtCore.Qt.ElideRight, width) + if elided_text != text: + label.setToolTip(text) + if markup is not None: + elided_text = markup % elided_text + label.setText(elided_text) diff -Nru ubuntu-sso-client-2.99.90/ubuntu_sso/qt/loadingoverlay.py ubuntu-sso-client-2.99.91/ubuntu_sso/qt/loadingoverlay.py --- ubuntu-sso-client-2.99.90/ubuntu_sso/qt/loadingoverlay.py 2012-03-06 18:01:25.000000000 +0000 +++ ubuntu-sso-client-2.99.91/ubuntu_sso/qt/loadingoverlay.py 2012-03-21 13:41:45.000000000 +0000 @@ -21,6 +21,8 @@ from ubuntu_sso.qt.ui import loadingoverlay_ui from ubuntu_sso.utils.ui import LOADING_OVERLAY +LOADING_STYLE = u'{0}' + class LoadingOverlay(QtGui.QFrame): """The widget that shows a loading animation and disable the widget below. @@ -43,7 +45,7 @@ self.counter = 0 self.orientation = False - self.ui.label.setText(LOADING_OVERLAY) + self.ui.label.setText(LOADING_STYLE.format(LOADING_OVERLAY)) # Invalid name "paintEvent", "eventFilter", "showEvent", "timerEvent" # pylint: disable=C0103 diff -Nru ubuntu-sso-client-2.99.90/ubuntu_sso/qt/main.py ubuntu-sso-client-2.99.91/ubuntu_sso/qt/main.py --- ubuntu-sso-client-2.99.90/ubuntu_sso/qt/main.py 2012-03-06 18:01:25.000000000 +0000 +++ ubuntu-sso-client-2.99.91/ubuntu_sso/qt/main.py 2012-03-21 13:41:45.000000000 +0000 @@ -25,6 +25,7 @@ from ubuntu_sso.qt.ui import resources_rc # pylint: enable=W0611 from ubuntu_sso.qt.ubuntu_sso_wizard import UbuntuSSOClientGUI +from ubuntu_sso.utils import PLATFORM_QSS def main(**kwargs): @@ -34,9 +35,17 @@ QtGui.QFontDatabase.addApplicationFont(':/Ubuntu-R.ttf') QtGui.QFontDatabase.addApplicationFont(':/Ubuntu-B.ttf') - # Apply Style Sheet -- The windows version may be different - qss = QtCore.QResource(":/stylesheet.qss") - app.setStyleSheet(qss.data()) + data = [] + for qss_name in (PLATFORM_QSS, ":/stylesheet.qss"): + qss = QtCore.QResource(qss_name) + data.append(unicode(qss.data())) + app.setStyleSheet('\n'.join(data)) + + # Fix the string that contains unicode chars. + for key in kwargs: + value = kwargs[key] + if isinstance(value, str): + kwargs[key] = value.decode('utf-8') # Unused variable 'ui', pylint: disable=W0612 ui = UbuntuSSOClientGUI(close_callback=app.exit, **kwargs) diff -Nru ubuntu-sso-client-2.99.90/ubuntu_sso/qt/network_detection_page.py ubuntu-sso-client-2.99.91/ubuntu_sso/qt/network_detection_page.py --- ubuntu-sso-client-2.99.90/ubuntu_sso/qt/network_detection_page.py 2012-03-06 18:01:25.000000000 +0000 +++ ubuntu-sso-client-2.99.91/ubuntu_sso/qt/network_detection_page.py 2012-03-21 13:41:45.000000000 +0000 @@ -20,7 +20,7 @@ from PyQt4 import QtGui from ubuntu_sso import networkstate - +from ubuntu_sso.logger import setup_logging from ubuntu_sso.qt.sso_wizard_page import SSOWizardPage from ubuntu_sso.qt.ui import network_detection_ui from ubuntu_sso.utils.ui import ( @@ -31,6 +31,9 @@ ) +logger = setup_logging('ubuntu_sso.network_detection_page') + + class NetworkDetectionPage(SSOWizardPage): """Widget to show if we don't detect a network connection.""" @@ -48,6 +51,7 @@ def initializePage(self): """Set UI details.""" + logger.debug('initializePage - About to show NetworkDetectionPage') self.wizard()._next_id = None self.setButtonText(QtGui.QWizard.CustomButton1, TRY_AGAIN_BUTTON) diff -Nru ubuntu-sso-client-2.99.90/ubuntu_sso/qt/proxy_dialog.py ubuntu-sso-client-2.99.91/ubuntu_sso/qt/proxy_dialog.py --- ubuntu-sso-client-2.99.90/ubuntu_sso/qt/proxy_dialog.py 2012-03-06 18:01:25.000000000 +0000 +++ ubuntu-sso-client-2.99.91/ubuntu_sso/qt/proxy_dialog.py 2012-03-21 13:41:45.000000000 +0000 @@ -21,6 +21,7 @@ from PyQt4.QtGui import QApplication, QDialog, QIcon from twisted.internet import defer +from ubuntu_sso import EXCEPTION_RAISED, USER_SUCCESS, USER_CANCELLATION from ubuntu_sso.logger import setup_gui_logging from ubuntu_sso.keyring import Keyring from ubuntu_sso.qt.ui.proxy_credentials_dialog_ui import Ui_ProxyCredsDialog @@ -37,10 +38,6 @@ PROXY_CREDS_SAVE_BUTTON, ) -CREDS_ACQUIRED = 0 -USER_CANCELATION = -1 -EXCEPTION_RAISED = -2 - logger = setup_gui_logging("ubuntu_sso.qt.proxy_dialog") @@ -107,15 +104,15 @@ logger.debug('Save credentials as for domain %s.', self.domain) yield self.keyring.set_credentials(self.domain, creds) except Exception, e: - logger.error('Could not retrieve credentials.') + logger.exception('Could not set credentials:') self.done(EXCEPTION_RAISED) # pylint: disable=W0703, W0612 - self.done(CREDS_ACQUIRED) + self.done(USER_SUCCESS) def _on_cancel_clicked(self, *args): """End the dialog.""" logger.debug('User canceled credentials dialog.') - self.done(USER_CANCELATION) + self.done(USER_CANCELLATION) def _set_buttons(self): """Set the labels of the buttons.""" diff -Nru ubuntu-sso-client-2.99.90/ubuntu_sso/qt/reset_password_page.py ubuntu-sso-client-2.99.91/ubuntu_sso/qt/reset_password_page.py --- ubuntu-sso-client-2.99.90/ubuntu_sso/qt/reset_password_page.py 2012-03-06 18:01:25.000000000 +0000 +++ ubuntu-sso-client-2.99.91/ubuntu_sso/qt/reset_password_page.py 2012-03-21 13:41:45.000000000 +0000 @@ -18,7 +18,7 @@ from functools import partial -from PyQt4.QtCore import SIGNAL, pyqtSignal +from PyQt4.QtCore import Qt, SIGNAL, pyqtSignal from PyQt4.QtGui import QApplication from ubuntu_sso import NO_OP @@ -73,7 +73,9 @@ def initializePage(self): """Extends QWizardPage initializePage method.""" + logger.debug('initializePage - About to show ResetPasswordPage') super(ResetPasswordPage, self).initializePage() + self.ui.gridLayout.setAlignment(Qt.AlignLeft) common.password_default_assistance(self.ui.password_assistance) self.ui.password_assistance.setVisible(False) self.setTitle(RESET_TITLE) @@ -158,6 +160,8 @@ def on_password_changed(self, app_name, email): """Let user know that the password was changed.""" + logger.info('ResetPasswordPage.on_password_changed for %s, email: %s', + app_name, email) self.hide_overlay() email = unicode(self.wizard().forgotten.ui.email_line_edit.text()) self.passwordChanged.emit(email) @@ -166,7 +170,7 @@ """Let the user know that there was an error.""" logger.error('Got error changing password for %s, error: %s', self.app_name, error) - self.show_error(self.app_name, build_general_error_message(error)) + self.show_error(build_general_error_message(error)) def set_new_password(self): """Request a new password to be set.""" diff -Nru ubuntu-sso-client-2.99.90/ubuntu_sso/qt/setup_account_page.py ubuntu-sso-client-2.99.91/ubuntu_sso/qt/setup_account_page.py --- ubuntu-sso-client-2.99.90/ubuntu_sso/qt/setup_account_page.py 2012-03-06 18:01:25.000000000 +0000 +++ ubuntu-sso-client-2.99.91/ubuntu_sso/qt/setup_account_page.py 2012-03-21 13:41:45.000000000 +0000 @@ -31,7 +31,7 @@ from PyQt4 import QtGui, QtCore from ubuntu_sso import NO_OP -from ubuntu_sso.logger import setup_gui_logging +from ubuntu_sso.logger import setup_gui_logging, log_call from ubuntu_sso.qt import ( LINK_STYLE, build_general_error_message, @@ -112,11 +112,17 @@ } return result + @property + def password(self): + """Return the content of the password edit.""" + return unicode(self.ui.password_edit.text()) + # Invalid name "initializePage" # pylint: disable=C0103 def initializePage(self): """Setup UI details.""" + logger.debug('initializePage - About to show SetupAccountPage') # Set Setup Account button self.wizard().setOption(QtGui.QWizard.HaveCustomButton3, True) try: @@ -152,7 +158,6 @@ def _set_translated_strings(self): """Set the strings.""" - logger.debug('SetUpAccountPage._set_translated_strings') # set the translated string title_page = REGISTER_TITLE.format(app_name=self.app_name) self.setTitle(title_page) @@ -190,7 +195,7 @@ terms = AGREE_TO_PRIVACY_POLICY.format(app_name=self.app_name, privacy_policy=privacy_policy_link) - self.terms_checkbox = enhanced_check_box.EnhancedCheckBox(terms) + self.terms_checkbox = enhanced_check_box.EnhancedCheckBox(terms, self) self.ui.hlayout_check.addWidget(self.terms_checkbox) self.terms_checkbox.setVisible(bool(self.tc_url or self.policy_url)) @@ -220,7 +225,6 @@ def _connect_ui(self): """Set the connection of signals.""" - logger.debug('SetUpAccountPage._connect_ui') self._set_line_edits_validations() self.ui.captcha_view.setPixmap(QtGui.QPixmap()) @@ -229,6 +233,7 @@ self.ui.password_assistance, common.NORMAL)) + self.ui.refresh_label.linkActivated.connect(self.hide_error) self.ui.refresh_label.linkActivated.connect(lambda url: \ self._refresh_captcha()) # We need to check if we enable the button on many signals @@ -271,7 +276,6 @@ def _refresh_captcha(self): """Refresh the captcha image shown in the ui.""" logger.debug('SetUpAccountPage._refresh_captcha') - self.hide_error() # lets clean behind us, do we have the old file arround? if self.captcha_file and os.path.exists(self.captcha_file): os.unlink(self.captcha_file) @@ -297,11 +301,9 @@ self.registerField('email_address', self.ui.email_edit) self.registerField('password', self.ui.password_edit) + @log_call(logger.debug) def on_captcha_generated(self, app_name, result): """A new image was generated.""" - logger.debug('SetUpAccountPage.on_captcha_generated for %r ' - '(captcha id %r, filename %r).', - app_name, result, self.captcha_file) self.captcha_id = result # HACK: First, let me apologize before hand, you can mention my mother # if needed I would do the same (mandel) @@ -320,28 +322,28 @@ self.captcha_image = pixmap_image self.on_captcha_refresh_complete() - def on_captcha_generation_error(self, error, *args, **kwargs): + @log_call(logger.error) + def on_captcha_generation_error(self, app_name, error): """An error ocurred.""" - logger.debug('SetUpAccountPage.on_captcha_generation_error') - self.show_error(self.app_name, CAPTCHA_LOAD_ERROR) + self.show_error(CAPTCHA_LOAD_ERROR) self.on_captcha_refresh_complete() + @log_call(logger.error) def on_user_registration_error(self, app_name, error): """Let the user know we could not register.""" - logger.debug('SetUpAccountPage.on_user_registration_error') # errors are returned as a dict with the data we want to show. msg = error.pop(ERROR_EMAIL, '') if msg: self.set_error_message(self.ui.email_assistance, msg) error_msg = build_general_error_message(error) if error_msg: - self.show_error(self.app_name, error_msg) + self.show_error(error_msg) self._refresh_captcha() + @log_call(logger.info) def on_user_registered(self, app_name, email): """Execute when the user did register.""" self.hide_overlay() - logger.debug('SetUpAccountPage.on_user_registered') email = unicode(self.ui.email_edit.text()) self.userRegistered.emit(email) @@ -376,7 +378,7 @@ messages.append(CAPTCHA_REQUIRED_ERROR) if len(messages) > 0: condition = False - self.show_error(self.app_name, '\n'.join(messages)) + self.show_error('\n'.join(messages)) return condition def set_next_validation(self): @@ -401,17 +403,14 @@ def is_correct_email(self, email_address): """Return if the email is correct.""" - logger.debug('SetUpAccountPage.is_correct_email') return '@' in email_address def is_correct_email_confirmation(self, email_address): """Return that the email is the same.""" - logger.debug('SetUpAccountPage.is_correct_email_confirmation') return unicode(self.ui.email_edit.text()) == email_address def is_correct_password_confirmation(self, password): """Return that the passwords are correct.""" - logger.debug('SetUpAccountPage.is_correct_password_confirmation') return unicode(self.ui.password_edit.text()) == password def focus_changed(self, old, now): @@ -500,12 +499,14 @@ def on_captcha_refreshing(self): """Show overlay when captcha is refreshing.""" + logger.info('SetUpAccountPage.on_captcha_refreshing') if self.isVisible(): self.show_overlay() self.captcha_received = False def on_captcha_refresh_complete(self): """Hide overlay when captcha finished refreshing.""" + logger.info('SetUpAccountPage.on_captcha_refresh_complete') self.hide_overlay() self.captcha_received = True diff -Nru ubuntu-sso-client-2.99.90/ubuntu_sso/qt/sso_wizard_page.py ubuntu-sso-client-2.99.91/ubuntu_sso/qt/sso_wizard_page.py --- ubuntu-sso-client-2.99.90/ubuntu_sso/qt/sso_wizard_page.py 2012-03-06 18:01:25.000000000 +0000 +++ ubuntu-sso-client-2.99.91/ubuntu_sso/qt/sso_wizard_page.py 2012-03-21 13:41:45.000000000 +0000 @@ -23,31 +23,38 @@ QApplication, QCursor, QFrame, - QFontMetrics, QHBoxLayout, - QVBoxLayout, + QLabel, QStyle, + QVBoxLayout, QWizardPage, - QLabel, ) from twisted.internet import defer from ubuntu_sso import main -from ubuntu_sso.logger import setup_gui_logging -from ubuntu_sso.qt import PREFERED_UI_SIZE, TITLE_STYLE +from ubuntu_sso.logger import setup_gui_logging, log_call +from ubuntu_sso.qt import ( + ERROR_STYLE, + maybe_elide_text, + PREFERED_UI_SIZE, + TITLE_STYLE, +) from ubuntu_sso.utils.ui import GENERIC_BACKEND_ERROR logger = setup_gui_logging('ubuntu_sso.sso_wizard_page') -class Header(QFrame): - """Header Class for Title and Subtitle in all wizard pages.""" +class WizardHeader(QFrame): + """WizardHeader Class for Title and Subtitle in all wizard pages.""" - def __init__(self): + def __init__(self, max_width, parent=None): """Create a new instance.""" - super(Header, self).__init__() - self.setObjectName('header') + super(WizardHeader, self).__init__(parent=parent) + self.max_width = max_width + self.max_title_width = self.max_width * 0.95 + self.max_subtitle_width = self.max_width * 1.8 + vbox = QVBoxLayout(self) vbox.setContentsMargins(0, 0, 0, 0) self.title_label = QLabel() @@ -64,11 +71,8 @@ def set_title(self, title): """Set the Title of the page or hide it otherwise""" if title: - fm = QFontMetrics(self.subtitle_label.font()) - width = PREFERED_UI_SIZE['width'] * 0.95 - elided_text = fm.elidedText(title, Qt.ElideRight, width) - self.title_label.setToolTip(title) - self.title_label.setText(elided_text) + maybe_elide_text(self.title_label, title, self.max_title_width, + markup=TITLE_STYLE) self.title_label.setVisible(True) else: self.title_label.setVisible(False) @@ -76,28 +80,23 @@ def set_subtitle(self, subtitle): """Set the Subtitle of the page or hide it otherwise""" if subtitle: - fm = QFontMetrics(self.subtitle_label.font()) - width = PREFERED_UI_SIZE['width'] * 1.8 - elided_text = fm.elidedText(subtitle, Qt.ElideRight, width) - self.subtitle_label.setText(elided_text) - self.subtitle_label.setToolTip(subtitle) + maybe_elide_text(self.subtitle_label, subtitle, + self.max_subtitle_width) self.subtitle_label.setVisible(True) else: self.subtitle_label.setVisible(False) -class SSOWizardPage(QWizardPage): - """Root class for all wizard pages.""" +class BaseWizardPage(QWizardPage): + """Base class for all wizard pages.""" ui_class = None - _signals = {} # override in children + max_width = 0 processingStarted = pyqtSignal() processingFinished = pyqtSignal() - def __init__(self, app_name, **kwargs): - """Create a new instance.""" - parent = kwargs.pop('parent', None) - super(SSOWizardPage, self).__init__(parent=parent) + def __init__(self, parent=None): + super(BaseWizardPage, self).__init__(parent=parent) self.ui = None if self.ui_class is not None: @@ -105,64 +104,39 @@ self.ui = self.ui_class() self.ui.setupUi(self) - # store common useful data provided by the app - self.app_name = app_name - self.ping_url = kwargs.get('ping_url', '') - self.tc_url = kwargs.get('tc_url', '') - self.policy_url = kwargs.get('policy_url', '') - self.help_text = kwargs.get('help_text', '') + if self.layout() is None: + self.setLayout(QVBoxLayout(self)) # Set the error area - self.form_errors_label = QLabel(' ') + self.form_errors_label = QLabel() self.form_errors_label.setObjectName('form_errors') self.form_errors_label.setAlignment(Qt.AlignBottom) self.layout().insertWidget(0, self.form_errors_label) + # Set the header - self.header = Header() + self.header = WizardHeader(max_width=self.max_width) self.header.set_title(title='') self.header.set_subtitle(subtitle='') self.layout().insertWidget(0, self.header) - self._signals_receivers = {} - self.backend = None self.layout().setAlignment(Qt.AlignLeft) - self.setup_page() - - def show_error(self, app_name, message): - """Show an error message inside the page.""" - self.hide_overlay() - fm = QFontMetrics(self.form_errors_label.font()) - width = PREFERED_UI_SIZE['width'] * 0.95 - elided_text = fm.elidedText(message, Qt.ElideRight, width) - self.form_errors_label.setText(elided_text) - self.form_errors_label.setToolTip(message) - - def hide_error(self): - """Hide the label errors in the current page.""" - # We actually want the label with one chat, because if it is an - # empty string, the height of the label is 0 - self.form_errors_label.setText(' ') + self._is_processing = False - def hide_overlay(self): - """Emit the signal to notify the upper container that ends loading.""" - self.setEnabled(True) - self.processingFinished.emit() - - def show_overlay(self): - """Emit the signal to notify the upper container that is loading.""" - self.setEnabled(False) - self.processingStarted.emit() - - @defer.inlineCallbacks - def setup_page(self): - """Setup the widget components.""" - client = yield main.get_sso_client() - self.backend = client.sso_login + def _get_is_processing(self): + """Is this widget processing any request?""" + return self._is_processing + + def _set_is_processing(self, new_value): + """Set this widget to be processing a request.""" + self._is_processing = new_value + self.setEnabled(not new_value) + if not self._is_processing: + self.processingFinished.emit() + else: + self.processingStarted.emit() - self._setup_signals() - self._set_translated_strings() - self._connect_ui() + is_processing = property(fget=_get_is_processing, fset=_set_is_processing) # pylint: disable=C0103 @@ -172,7 +146,7 @@ def setTitle(self, title=''): """Set the Wizard Page Title.""" - self.header.set_title(TITLE_STYLE % title) + self.header.set_title(title) def setSubTitle(self, subtitle=''): """Set the Wizard Page Subtitle.""" @@ -188,6 +162,74 @@ # pylint: enable=C0103 + @log_call(logger.error) + def show_error(self, message): + """Show an error message inside the page.""" + self.is_processing = False + maybe_elide_text(self.form_errors_label, message, + self.max_width * 0.95, markup=ERROR_STYLE) + + def hide_error(self): + """Hide the label errors in the current page.""" + # We actually want the label with one empty char, because if it is an + # empty string, the height of the label is 0 + self.form_errors_label.setText(' ') + + +class SSOWizardPage(BaseWizardPage): + """Root class for all SSO specific wizard pages.""" + + _signals = {} # override in children + max_width = PREFERED_UI_SIZE['width'] + + def __init__(self, app_name, **kwargs): + """Create a new instance.""" + parent = kwargs.pop('parent', None) + super(SSOWizardPage, self).__init__(parent=parent) + + # store common useful data provided by the app + self.app_name = app_name + self.ping_url = kwargs.get('ping_url', '') + self.tc_url = kwargs.get('tc_url', '') + self.policy_url = kwargs.get('policy_url', '') + self.help_text = kwargs.get('help_text', '') + + self._signals_receivers = {} + self.backend = None + + self.setup_page() + + def hide_overlay(self): + """Emit the signal to notify the upper container that ends loading.""" + self.is_processing = False + + def show_overlay(self): + """Emit the signal to notify the upper container that is loading.""" + self.is_processing = True + + @defer.inlineCallbacks + def setup_page(self): + """Setup the widget components.""" + logger.info('Starting setup_page for: %r', self) + # pylint: disable=W0702,W0703 + try: + # Get Backend + client = yield main.get_sso_client() + self.backend = client.sso_login + self._set_translated_strings() + self._connect_ui() + # Call _setup_signals at the end, so we ensure that the UI + # is at least styled as expected if the operations with the + # backend fails. + self._setup_signals() + except: + message = 'There was a problem trying to setup the page %r' % self + self.show_error(message) + logger.exception(message) + self.setEnabled(False) + # pylint: enable=W0702,W0703 + logger.info('%r - setup_page ends, backend is %r.', self, self.backend) + def _filter_by_app_name(self, f): """Excecute the decorated function only for 'self.app_name'.""" @@ -218,11 +260,9 @@ def _set_translated_strings(self): """Implement in each child.""" - raise NotImplementedError() def _connect_ui(self): """Implement in each child.""" - raise NotImplementedError() def _handle_error(self, remote_call, handler, error): """Handle any error when calling the remote backend.""" diff -Nru ubuntu-sso-client-2.99.90/ubuntu_sso/qt/tests/__init__.py ubuntu-sso-client-2.99.91/ubuntu_sso/qt/tests/__init__.py --- ubuntu-sso-client-2.99.90/ubuntu_sso/qt/tests/__init__.py 2012-03-06 18:01:25.000000000 +0000 +++ ubuntu-sso-client-2.99.91/ubuntu_sso/qt/tests/__init__.py 2012-03-21 13:41:45.000000000 +0000 @@ -21,7 +21,7 @@ from twisted.trial.unittest import TestCase from ubuntu_sso import main, NO_OP -from ubuntu_sso.qt import TITLE_STYLE +from ubuntu_sso.qt import ERROR_STYLE, maybe_elide_text, TITLE_STYLE from ubuntu_sso.tests import ( APP_NAME, HELP_TEXT, @@ -38,6 +38,15 @@ # pylint: disable=W0212 +def build_string_for_pixels(label, width): + """Return a random string that will be as big as with in pixels.""" + char = 'a' + fm = QtGui.QFontMetrics(label.font()) + pixel_width = fm.width(char) + chars = int(width / pixel_width) + return char * chars + + class FakedObject(object): """Fake an object, record every call.""" @@ -439,6 +448,9 @@ self.wizard = self.ui_wizard_class() self.patch(self.ui, 'wizard', lambda: self.wizard) + self.ui.show() + self.addCleanup(self.ui.hide) + def _set_called(self, *args, **kwargs): """Store 'args' and 'kwargs' for test assertions.""" self._called = (args, kwargs) @@ -467,6 +479,38 @@ self.assertEqual(self.signal_results, [signal_args]) + def assert_title_correct(self, title_label, expected, max_width): + """Check that the label's text is equal to 'expected'.""" + label = QtGui.QLabel() + maybe_elide_text(label, expected, max_width) + + self.assertEqual(TITLE_STYLE % unicode(label.text()), + unicode(title_label.text())) + self.assertEqual(unicode(label.toolTip()), + unicode(title_label.toolTip())) + self.assertTrue(title_label.isVisible()) + + def assert_subtitle_correct(self, subtitle_label, expected, max_width): + """Check that the subtitle is equal to 'expected'.""" + label = QtGui.QLabel() + maybe_elide_text(label, expected, max_width) + + self.assertEqual(unicode(label.text()), unicode(subtitle_label.text())) + self.assertEqual(unicode(label.toolTip()), + unicode(subtitle_label.toolTip())) + self.assertTrue(subtitle_label.isVisible()) + + def assert_error_correct(self, error_label, expected, max_width): + """Check that the error 'error_label' displays 'expected' as text.""" + label = QtGui.QLabel() + maybe_elide_text(label, expected, max_width) + + self.assertEqual(ERROR_STYLE % unicode(label.text()), + unicode(error_label.text())) + self.assertEqual(unicode(label.toolTip()), + unicode(error_label.toolTip())) + self.assertTrue(error_label.isVisible()) + def get_pixmap_data(self, pixmap): """Get the raw data of a QPixmap.""" byte_array = QtCore.QByteArray() @@ -507,7 +551,7 @@ self.assertIn(signal, self.ui._signals) self.assertTrue(callable(self.ui._signals[signal])) - expected = ['_setup_signals', '_set_translated_strings', '_connect_ui'] + expected = ['_set_translated_strings', '_connect_ui', '_setup_signals'] self.assertEqual(expected, called) @@ -550,14 +594,18 @@ self.assertEqual(self._overlay_hide_counter, 1) self.assertTrue(self.ui.isEnabled()) + # pylint: disable=W0221 + def assert_title_correct(self, expected): """Check that the title is equal to 'expected'.""" - self.assertEqual(TITLE_STYLE % expected, unicode(self.ui.title())) + check = super(PageBaseTestCase, self).assert_title_correct + check(self.ui.header.title_label, expected, + self.ui.header.max_title_width) def assert_subtitle_correct(self, expected): """Check that the subtitle is equal to 'expected'.""" - elided_text = unicode(self.ui.subTitle()) - elided_text = elided_text[:len(elided_text) - 1] + check = super(PageBaseTestCase, self).assert_subtitle_correct + check(self.ui.header.subtitle_label, expected, + self.ui.header.max_subtitle_width) - self.assertTrue(expected.startswith(elided_text)) - self.assertEqual(self.ui.header.subtitle_label.toolTip(), expected) + # pylint: enable=W0221 diff -Nru ubuntu-sso-client-2.99.90/ubuntu_sso/qt/tests/login_u_p.py ubuntu-sso-client-2.99.91/ubuntu_sso/qt/tests/login_u_p.py --- ubuntu-sso-client-2.99.90/ubuntu_sso/qt/tests/login_u_p.py 2012-03-06 18:01:25.000000000 +0000 +++ ubuntu-sso-client-2.99.91/ubuntu_sso/qt/tests/login_u_p.py 2012-03-21 13:41:45.000000000 +0000 @@ -39,7 +39,6 @@ def found(*args): """The result was received.""" - print "result received", args d.callback(args) client.cred_manager.connect_to_signal('CredentialsFound', found) @@ -61,7 +60,6 @@ yield cleared yield client.cred_manager.login_email_password('SUPER', args) - print "called ok" yield d yield client.disconnect() diff -Nru ubuntu-sso-client-2.99.90/ubuntu_sso/qt/tests/show_gui.py ubuntu-sso-client-2.99.91/ubuntu_sso/qt/tests/show_gui.py --- ubuntu-sso-client-2.99.90/ubuntu_sso/qt/tests/show_gui.py 2012-03-06 18:01:25.000000000 +0000 +++ ubuntu-sso-client-2.99.91/ubuntu_sso/qt/tests/show_gui.py 2012-03-21 13:41:45.000000000 +0000 @@ -46,7 +46,6 @@ def found(*args): """The result was received.""" - print "result received", args d.callback(args) client.cred_manager.connect_to_signal('CredentialsFound', found) @@ -62,7 +61,6 @@ TC_URL_KEY: u'http://www.google.com/', UI_EXECUTABLE_KEY: 'ubuntu-sso-login-gtk', }) - print "called ok" yield d yield client.disconnect() diff -Nru ubuntu-sso-client-2.99.90/ubuntu_sso/qt/tests/test_common.py ubuntu-sso-client-2.99.91/ubuntu_sso/qt/tests/test_common.py --- ubuntu-sso-client-2.99.90/ubuntu_sso/qt/tests/test_common.py 2012-03-06 18:01:25.000000000 +0000 +++ ubuntu-sso-client-2.99.91/ubuntu_sso/qt/tests/test_common.py 2012-03-21 13:41:45.000000000 +0000 @@ -20,7 +20,13 @@ from twisted.internet import defer from twisted.trial.unittest import TestCase -from ubuntu_sso.qt.common import (check_as_invalid, +from ubuntu_sso.qt import ( + build_general_error_message, + maybe_elide_text, + GENERIC_BACKEND_ERROR, +) +from ubuntu_sso.qt.common import ( + check_as_invalid, check_as_valid, password_assistance, password_check_match, @@ -30,7 +36,9 @@ PASSWORD_DIGIT, PASSWORD_LENGTH, PASSWORD_MATCH, - PASSWORD_UPPER) + PASSWORD_UPPER, +) +from ubuntu_sso.qt.tests import build_string_for_pixels class PasswordTestCase(TestCase): @@ -234,3 +242,123 @@ line_edit = QtGui.QLineEdit() check_as_invalid(line_edit) self.assertTrue(line_edit.property("formError").toBool()) + + +class ElidedTextTestCase(TestCase): + """The test case for the maybe_elide_text function.""" + + max_width = 100 + + @defer.inlineCallbacks + def setUp(self): + """Setup tests.""" + yield super(ElidedTextTestCase, self).setUp() + self.ui = QtGui.QLabel() + + def test_text_not_elided_if_too_short(self): + """If text is shorter than max_width, do not elide.""" + text = build_string_for_pixels(self.ui, self.max_width - 1) + + maybe_elide_text(self.ui, text, self.max_width) + + self.assertEqual(self.ui.toolTip(), '') + self.assertEqual(self.ui.text(), text) + self.assertNotIn(u'\u2026', self.ui.text()) + + def test_text_not_elided_if_equals_max_width(self): + """If text is equal than max_width, do not elide.""" + text = build_string_for_pixels(self.ui, self.max_width) + + maybe_elide_text(self.ui, text, self.max_width) + + self.assertEqual(self.ui.toolTip(), '') + self.assertEqual(self.ui.text(), text) + self.assertNotIn(u'\u2026', self.ui.text()) + + def test_text_elided_if_bigger_than_max_width(self): + """If text is equal than max_width, do not elide.""" + text = build_string_for_pixels(self.ui, self.max_width + 10) + + maybe_elide_text(self.ui, text, self.max_width) + + self.assertEqual(self.ui.toolTip(), text) + expected = unicode(self.ui.text()) + self.assertTrue(expected.endswith(u'\u2026')) + self.assertTrue(text.startswith(expected[:-1])) + + +class BuildGeneralErrorMessageTestCase(TestCase): + """Test passwords conditions.""" + + def test_with_message(self): + """Test build_general_error_message with 'message' key.""" + error = "error message" + err_dict = {'message': error} + + result = build_general_error_message(err_dict) + + self.assertEqual(result, error) + + def test_with_all(self): + """Test build_general_error_message with 'all' key.""" + error = "error message" + err_dict = {'__all__': error} + + result = build_general_error_message(err_dict) + + self.assertEqual(result, error) + + def test_with_message_and_all(self): + """Test build_general_error_message with 'all' and 'message' key.""" + error = "error message" + error2 = "error message2" + err_dict = {'__all__': error, 'message': error2} + + result = build_general_error_message(err_dict) + + expected = '\n'.join((error, error2)) + self.assertEqual(result, expected) + + def test_with_all_and_error_message(self): + """Test for 'all' and 'error_message' key.""" + error = "error message" + error2 = "error message2" + err_dict = {'__all__': error, 'error_message': error2} + result = build_general_error_message(err_dict) + expected = '\n'.join((error, error2)) + self.assertEqual(result, expected) + + def test_with_random_keys(self): + """Test build_general_error_message with random keys.""" + error = "error message" + error2 = "error message2" + err_dict = {'my_bad': error, 'odd_error': error2} + + result = build_general_error_message(err_dict) + + expected = '\n'.join( + [('%s: %s' % (k, v)) for k, v in err_dict.iteritems()]) + self.assertEqual(result, expected) + + def test_with_random_keys_with_errtype(self): + """Test build_general_error_message with random keys and errtype.""" + error = "error message" + error2 = "error message2" + err_dict = {'my_bad': error, 'odd_error': error2, 'errtype': 'Danger'} + + result = build_general_error_message(err_dict) + + expected = '\n'.join( + [('%s: %s' % (k, v)) \ + for k, v in {'my_bad': error, 'odd_error': error2}.iteritems()]) + self.assertEqual(result, expected) + + def test_with_not_dict(self): + """Test build_general_error_message with argument not dict.""" + error = "error message" + err_dict = Exception(error) + + result = build_general_error_message(err_dict) + + expected = GENERIC_BACKEND_ERROR + self.assertEqual(result, expected) diff -Nru ubuntu-sso-client-2.99.90/ubuntu_sso/qt/tests/test_current_user_sign_in_page.py ubuntu-sso-client-2.99.91/ubuntu_sso/qt/tests/test_current_user_sign_in_page.py --- ubuntu-sso-client-2.99.90/ubuntu_sso/qt/tests/test_current_user_sign_in_page.py 2012-03-06 18:01:25.000000000 +0000 +++ ubuntu-sso-client-2.99.91/ubuntu_sso/qt/tests/test_current_user_sign_in_page.py 2012-03-21 13:41:45.000000000 +0000 @@ -24,6 +24,7 @@ FakePageUiStyle, FakeWizardButtonStyle, ) +from ubuntu_sso.tests import EMAIL # pylint: disable=W0212 @@ -51,19 +52,32 @@ self.assertTrue(button.properties['default']) self.assertFalse(button.isEnabled()) + def test_unicode_in_forgotten_password_link(self): + """Ensure that this label supports unicode.""" + forgot_fr = u"J'ai oublié mon mot de passe" + self.patch(gui, "FORGOTTEN_PASSWORD_BUTTON", forgot_fr) + forgotten_text = gui.LINK_STYLE.format(link_url='#', + link_text=forgot_fr) + self.ui._set_translated_strings() + self.assertEqual(unicode(self.ui.ui.forgot_password_label.text()), + forgotten_text) + def test_set_translated_strings(self): """Test the translated string method.""" expected = gui.LOGIN_TITLE.format(app_name=self.app_name) self.assert_title_correct(expected) expected = gui.LOGIN_SUBTITLE % dict(app_name=self.app_name) self.assert_subtitle_correct(expected) - self.assertEqual(self.ui.ui.email_label.text(), gui.EMAIL_LABEL) - self.assertEqual(self.ui.ui.password_label.text(), + self.assertEqual(unicode(self.ui.ui.email_label.text()), + gui.EMAIL_LABEL) + self.assertEqual(unicode(self.ui.ui.password_label.text()), gui.LOGIN_PASSWORD_LABEL) text = gui.LINK_STYLE.format(link_url='#', link_text=gui.FORGOTTEN_PASSWORD_BUTTON) - self.assertEqual(self.ui.ui.forgot_password_label.text(), text) - self.assertEqual(self.ui.ui.sign_in_button.text(), gui.SIGN_IN_BUTTON) + self.assertEqual(unicode(self.ui.ui.forgot_password_label.text()), + text) + self.assertEqual(unicode(self.ui.ui.sign_in_button.text()), + gui.SIGN_IN_BUTTON) def test_connect_ui(self): """Test the connect ui method.""" @@ -128,30 +142,27 @@ def test_on_login_error(self): """Test the on_login_error method.""" self.patch(self.ui, "show_error", self._set_called) - app_name = 'my_app' - self.ui.app_name = app_name error = {'errtype': 'UserNotValidated'} - self.ui.on_login_error(app_name, error) + + self.ui.on_login_error(self.app_name, error) + self.assertEqual(self._overlay_hide_counter, 0) self.assertTrue(self.ui.isEnabled()) - expected = ((self.ui, 'my_app', ''), {}) + expected = ((self.ui, self.app_name, ''), {}) self.assertTrue(expected, self._called) def test_on_logged_in(self): """Test the on_login_in method.""" - email = 'email@example' - self.ui.ui.email_edit.setText(email) - - self.assert_signal_emitted(self.ui.userLoggedIn, (email,), - self.ui.on_logged_in, self.app_name, email) + self.ui.ui.email_edit.setText(EMAIL) + self.assert_signal_emitted(self.ui.userLoggedIn, (EMAIL,), + self.ui.on_logged_in, self.app_name, EMAIL) self.assertTrue(self.ui.isEnabled()) def test_on_user_not_validated(self): """Test the navigation flow on user not validated.""" - email = 'email@example' - self.ui.ui.email_edit.setText(email) - self.assert_signal_emitted(self.ui.userNotValidated, (email,), - self.ui.on_user_not_validated, self.app_name, email) + self.ui.ui.email_edit.setText(EMAIL) + self.assert_signal_emitted(self.ui.userNotValidated, (EMAIL,), + self.ui.on_user_not_validated, self.app_name, EMAIL) def test_on_forgotten_password(self): """Test the on_forgotten_password method.""" diff -Nru ubuntu-sso-client-2.99.90/ubuntu_sso/qt/tests/test_email_verification.py ubuntu-sso-client-2.99.91/ubuntu_sso/qt/tests/test_email_verification.py --- ubuntu-sso-client-2.99.90/ubuntu_sso/qt/tests/test_email_verification.py 2012-03-06 18:01:25.000000000 +0000 +++ ubuntu-sso-client-2.99.91/ubuntu_sso/qt/tests/test_email_verification.py 2012-03-21 13:41:45.000000000 +0000 @@ -78,14 +78,12 @@ email = 'mail@example' self.ui.email = email self.ui.set_titles(email) - self.assertEqual(self.ui.header.title_label.text(), - email_verification_page.VERIFY_EMAIL_TITLE) + self.assert_title_correct(email_verification_page.VERIFY_EMAIL_TITLE) expected = email_verification_page.VERIFY_EMAIL_CONTENT % { "app_name": self.app_name, "email": email, } - self.assertEqual(unicode(self.ui.header.subtitle_label.toolTip()), - expected) + self.assert_subtitle_correct(expected) def test_initialize_page(self): """Test the initialization method.""" @@ -100,10 +98,11 @@ def test_on_email_validation_error(self): """Test the validate_email method.""" self.patch(self.ui, "show_error", self._set_called) - app_name = 'my_app' error = {email_verification_page: 'error in email_verification_page'} - self.ui.on_email_validation_error(app_name, error) - expected = ((self.ui, app_name, ''), {}) + + self.ui.on_email_validation_error(self.app_name, error) + + expected = ((self.ui, ''), {}) self.assertTrue(expected, self._called) self.assertEqual(self._overlay_hide_counter, 1) diff -Nru ubuntu-sso-client-2.99.90/ubuntu_sso/qt/tests/test_enhanced_check_box.py ubuntu-sso-client-2.99.91/ubuntu_sso/qt/tests/test_enhanced_check_box.py --- ubuntu-sso-client-2.99.90/ubuntu_sso/qt/tests/test_enhanced_check_box.py 2012-03-06 18:01:25.000000000 +0000 +++ ubuntu-sso-client-2.99.91/ubuntu_sso/qt/tests/test_enhanced_check_box.py 2012-03-21 13:41:45.000000000 +0000 @@ -16,7 +16,7 @@ """Tests for the EnhancedCheckBox widget.""" -from PyQt4 import QtCore +from PyQt4 import QtGui, QtCore from ubuntu_sso.qt import enhanced_check_box from ubuntu_sso.qt.tests import BaseTestCase @@ -40,3 +40,21 @@ check.setText("text") self.assertEqual(check.text(), "text") self.assertEqual(check.text(), check.text_label.text()) + + def test_enhanced_check_size_adjust_with_small_height(self): + """Check if the size of the EnhancedCheckBox is adjusted correctly.""" + text = 't' * 200 + height = 10 + widget = QtGui.QWidget() + widget.setFixedSize(200, height) + check = enhanced_check_box.EnhancedCheckBox(text, widget) + self.assertTrue(check.height() > height) + + def test_enhanced_check_size_adjust_with_big_height(self): + """Check if the size of the EnhancedCheckBox is adjusted correctly.""" + text = 't' * 20 + height = 200 + widget = QtGui.QWidget() + widget.setFixedSize(200, height) + check = enhanced_check_box.EnhancedCheckBox(text, widget) + self.assertTrue(check.height() < height) diff -Nru ubuntu-sso-client-2.99.90/ubuntu_sso/qt/tests/test_forgotten_password.py ubuntu-sso-client-2.99.91/ubuntu_sso/qt/tests/test_forgotten_password.py --- ubuntu-sso-client-2.99.90/ubuntu_sso/qt/tests/test_forgotten_password.py 2012-03-06 18:01:25.000000000 +0000 +++ ubuntu-sso-client-2.99.91/ubuntu_sso/qt/tests/test_forgotten_password.py 2012-03-21 13:41:45.000000000 +0000 @@ -75,9 +75,10 @@ subtitle = gui.FORGOTTEN_PASSWORD_SUBTITLE self.assert_subtitle_correct(subtitle.format(app_name=self.app_name)) - self.assertEqual(self.ui.ui.email_address_label.text(), + self.assertEqual(unicode(self.ui.ui.email_address_label.text()), gui.EMAIL_LABEL) - self.assertEqual(self.ui.ui.send_button.text(), gui.RESET_PASSWORD) + self.assertEqual(unicode(self.ui.ui.send_button.text()), + gui.RESET_PASSWORD) def test_connect_ui(self): """Test the connect ui method.""" @@ -112,7 +113,8 @@ """Test on_password_reset_error method.""" self.patch(self.ui, "show_error", self._set_called) error = {'errtype': 'FooBarBaz'} + self.ui.on_password_reset_error(self.app_name, error) - expected = ((self.ui, self.app_name, - gui.REQUEST_PASSWORD_TOKEN_WRONG_EMAIL), {}) + + expected = ((self.ui, gui.REQUEST_PASSWORD_TOKEN_WRONG_EMAIL), {}) self.assertTrue(expected, self._called) diff -Nru ubuntu-sso-client-2.99.90/ubuntu_sso/qt/tests/test_loadingoverlay.py ubuntu-sso-client-2.99.91/ubuntu_sso/qt/tests/test_loadingoverlay.py --- ubuntu-sso-client-2.99.90/ubuntu_sso/qt/tests/test_loadingoverlay.py 2012-03-06 18:01:25.000000000 +0000 +++ ubuntu-sso-client-2.99.91/ubuntu_sso/qt/tests/test_loadingoverlay.py 2012-03-21 13:41:45.000000000 +0000 @@ -29,8 +29,5 @@ def test_status_correct(self): """Test if the necessary variables for the animation exists""" - self.ui.show() - self.addCleanup(self.ui.hide) - self.assertTrue(self.ui.counter is not None) self.assertTrue(self.ui.orientation is not None) diff -Nru ubuntu-sso-client-2.99.90/ubuntu_sso/qt/tests/test_main.py ubuntu-sso-client-2.99.91/ubuntu_sso/qt/tests/test_main.py --- ubuntu-sso-client-2.99.90/ubuntu_sso/qt/tests/test_main.py 2012-03-06 18:01:25.000000000 +0000 +++ ubuntu-sso-client-2.99.91/ubuntu_sso/qt/tests/test_main.py 2012-03-21 13:41:45.000000000 +0000 @@ -16,27 +16,123 @@ """Tests for the main module.""" +from PyQt4 import QtCore +from twisted.internet import defer from twisted.trial.unittest import TestCase from ubuntu_sso.qt import main +from ubuntu_sso import tests + + +# pylint: disable=C0103 +class FakeUi(object): + + """A fake UI.""" + + def size(self): + """Fake size.""" + return QtCore.QSize(100, 100) + + def setGeometry(self, *args): + """Fake setGeometry.""" + + show = setGeometry + + +class FakeDesktop(object): + + """Fake Desktop Widget.""" + + def availableGeometry(self): + """Fake availableGeometry for desktop.-""" + return QtCore.QRect(100, 100, 100, 100) + + +class FakeApplication(object): + + """Fake QApplication.""" + + called = {} + __instance = None + + def __init__(self, args): + self.called['args'] = args + FakeApplication.__instance = self + + def setStyleSheet(self, style): + """Fake setStyleSheet.""" + self.called["setStyleSheet"] = style + + def styleSheet(self): + """Fake get style sheet.""" + return self.called.get("setStyleSheet", '') + + def desktop(self): + """Fake Desktop.""" + return FakeDesktop() + + def exec_(self): + """Fake exec_.""" + + def exit(self): + """Fake exit.""" + + @classmethod + def instance(cls): + """Fake instance.""" + return FakeApplication.__instance +# pylint: enable=C0103 class BasicTestCase(TestCase): """Test case with a helper tracker.""" + @defer.inlineCallbacks + def setUp(self): + yield super(BasicTestCase, self).setUp() + self.called = [] + + def called_ui(**kw): + """record ui call.""" + self.called.append(('GUI', kw)) + return FakeUi() + + self.patch(main, 'UbuntuSSOClientGUI', called_ui) + self.patch(main.QtGui, 'QApplication', FakeApplication) + def test_main(self): """Calling main.main() a UI instance is created.""" - called = [] - self.patch(main, 'UbuntuSSOClientGUI', - lambda **kw: called.append(('GUI', kw))) - self.patch(main.QtGui.QApplication, 'exec_', - lambda *a: called.append('main')) + kwargs = dict(app_name='APP_NAME', foo='foo', bar='bar', + baz='yadda', yadda=0) + main.main(**kwargs) - kwargs = dict(foo='foo', bar='bar', baz='yadda', yadda=0) + kwargs['close_callback'] = main.QtGui.QApplication.instance().exit + self.assertEqual(self.called, [('GUI', kwargs)]) + + def test_main_args(self): + """Calling main.main() a UI instance is created.""" + arg_list = (tests.APP_NAME, tests.NAME, tests.PASSWORD, + tests.EMAIL_TOKEN) + kwargs = dict(app_name=arg_list[0].encode('utf-8'), + foo=arg_list[1].encode('utf-8'), + bar=arg_list[2].encode('utf-8'), + baz=arg_list[3].encode('utf-8')) main.main(**kwargs) kwargs['close_callback'] = main.QtGui.QApplication.instance().exit - self.assertEqual(called, [('GUI', kwargs), 'main']) + expected = dict(app_name=arg_list[0], foo=arg_list[1], + bar=arg_list[2], baz=arg_list[3], + close_callback=main.QtGui.QApplication.instance().exit) + self.assertEqual(self.called, [('GUI', expected)]) - test_main.skip = 'Failing with QObject::startTimer: QTimer can only be ' \ - 'used with threads started with QThread' + def test_styles_load(self): + """Test that all stylesheets load.""" + kwargs = dict(foo='foo', bar='bar', baz='yadda', yadda=0) + main.main(**kwargs) + data = [] + for qss_name in (main.PLATFORM_QSS, ":/stylesheet.qss"): + qss = QtCore.QResource(qss_name) + data.append(unicode(qss.data())) + self.assertEqual( + unicode(main.QtGui.QApplication.instance().styleSheet()), + '\n'.join(data)) diff -Nru ubuntu-sso-client-2.99.90/ubuntu_sso/qt/tests/test_proxy_dialog.py ubuntu-sso-client-2.99.91/ubuntu_sso/qt/tests/test_proxy_dialog.py --- ubuntu-sso-client-2.99.90/ubuntu_sso/qt/tests/test_proxy_dialog.py 2012-03-06 18:01:25.000000000 +0000 +++ ubuntu-sso-client-2.99.91/ubuntu_sso/qt/tests/test_proxy_dialog.py 2012-03-21 13:41:45.000000000 +0000 @@ -256,7 +256,7 @@ self.patch(dialog, 'done', fake_done) dialog._on_cancel_clicked() - self.assertIn(('done', proxy_dialog.USER_CANCELATION), called) + self.assertIn(('done', proxy_dialog.USER_CANCELLATION), called) def assert_save_button(self, set_creds_callback, result_number): """Test the save button execution.""" @@ -288,7 +288,7 @@ def test_on_save_clicked_correct(self): """Test that we do save the creds.""" set_creds_cb = lambda: defer.succeed(True) - result_number = proxy_dialog.CREDS_ACQUIRED + result_number = proxy_dialog.USER_SUCCESS self.assert_save_button(set_creds_cb, result_number) diff -Nru ubuntu-sso-client-2.99.90/ubuntu_sso/qt/tests/test_reset_password.py ubuntu-sso-client-2.99.91/ubuntu_sso/qt/tests/test_reset_password.py --- ubuntu-sso-client-2.99.90/ubuntu_sso/qt/tests/test_reset_password.py 2012-03-06 18:01:25.000000000 +0000 +++ ubuntu-sso-client-2.99.91/ubuntu_sso/qt/tests/test_reset_password.py 2012-03-21 13:41:45.000000000 +0000 @@ -70,27 +70,23 @@ def test_initialize(self): """Check the Title and Subtitle.""" - self.ui.show() self.ui.initializePage() - self.addCleanup(self.ui.hide) self.assert_title_correct(RESET_TITLE) self.assert_subtitle_correct(RESET_SUBTITLE) - self.assertEqual(self.ui.ui.password_label.text(), PASSWORD1_ENTRY) - self.assertEqual(self.ui.ui.confirm_password_label.text(), + self.assertEqual(unicode(self.ui.ui.password_label.text()), + PASSWORD1_ENTRY) + self.assertEqual(unicode(self.ui.ui.confirm_password_label.text()), PASSWORD2_ENTRY) - self.assertEqual(self.ui.ui.reset_code.text(), RESET_CODE_ENTRY) + self.assertEqual(unicode(self.ui.ui.reset_code.text()), + RESET_CODE_ENTRY) def test_focus_changed_password_visibility(self): """Check visibility changes when focus_changed() is executed.""" - self.ui.show() - self.addCleanup(self.ui.hide) self.ui.focus_changed(None, self.ui.ui.password_line_edit) self.assertTrue(self.ui.ui.password_assistance.isVisible()) def test_show_hide_event(self): """Check connections to focusChanged on show and hide event.""" - self.ui.show() - self.addCleanup(self.ui.hide) self.assertEqual(QtGui.QApplication.instance().receivers( QtCore.SIGNAL("focusChanged(QWidget*, QWidget*)")), 1) self.ui.hide() @@ -102,24 +98,20 @@ def test_focus_changed_1(self): """Check functions execution when focus_changed() is executed.""" self.patch(common, 'password_default_assistance', self._set_called) - - self.ui.show() - self.addCleanup(self.ui.hide) - self.assertFalse(self._called) + self.ui.focus_changed(None, self.ui.ui.password_line_edit) + self.assertTrue(self.ui.ui.password_assistance.isVisible()) self.assertTrue(self._called) def test_focus_changed_2(self): """Check functions execution when focus_changed() is executed.""" self.patch(common, 'password_check_match', self._set_called) - - self.ui.show() - self.addCleanup(self.ui.hide) - self.assertFalse(self._called) + self.ui.focus_changed(None, self.ui.ui.confirm_password_line_edit) + self.assertTrue(self.ui.ui.password_assistance.isVisible()) self.assertTrue(self._called) diff -Nru ubuntu-sso-client-2.99.90/ubuntu_sso/qt/tests/test_setup_account.py ubuntu-sso-client-2.99.91/ubuntu_sso/qt/tests/test_setup_account.py --- ubuntu-sso-client-2.99.90/ubuntu_sso/qt/tests/test_setup_account.py 2012-03-06 18:01:25.000000000 +0000 +++ ubuntu-sso-client-2.99.91/ubuntu_sso/qt/tests/test_setup_account.py 2012-03-21 13:41:45.000000000 +0000 @@ -43,13 +43,20 @@ """ self.ui.ui.name_edit.setText("") self.ui.name_assistance() - self.ui.show() - self.addCleanup(self.ui.hide) self.assertTrue(self.ui.ui.name_assistance.isVisible()) - self.assertEqual( - unicode(self.ui.ui.name_assistance.text()), - gui.ERROR_STYLE % gui.EMPTY_NAME) - self.ui.hide() + self.assert_error_correct(self.ui.ui.name_assistance, gui.EMPTY_NAME, + max_width=self.ui.header.max_title_width) + + def test_hide_error_on_refresh_clicked(self): + """Hide form errors when the user click to refresh the captcha.""" + self.ui.show_error('error') + self.assert_error_correct(self.ui.form_errors_label, 'error', + max_width=self.ui.header.max_title_width) + + self.ui.ui.refresh_label.linkActivated.emit('error') + + message = unicode(self.ui.form_errors_label.text()) + self.assertEqual(message, ' ') def test_enable_setup_button_with_visible_check(self): """Test _enable_setup_button method with terms check visible.""" @@ -65,8 +72,6 @@ self.ui.ui.captcha_solution_edit.setText('captcha solution') self.ui.terms_checkbox.setChecked(True) - self.ui.show() - self.addCleanup(self.ui.hide) self.ui.terms_checkbox.setVisible(True) self.ui.ui.captcha_solution_edit.textEdited.emit('') self.assertTrue(self.ui.set_up_button.isEnabled()) @@ -84,8 +89,6 @@ self.ui.ui.confirm_password_edit.setText(password) self.ui.ui.captcha_solution_edit.setText('captcha solution') - self.ui.show() - self.addCleanup(self.ui.hide) self.ui.terms_checkbox.setVisible(False) self.ui.ui.captcha_solution_edit.textEdited.emit('') self.assertTrue(self.ui.set_up_button.isEnabled()) @@ -114,8 +117,6 @@ def test_password_focus_gain(self): """Check functions execution when focus_changed() is executed.""" - self.ui.show() - self.addCleanup(self.ui.hide) self.ui.ui.password_assistance.setVisible(False) self.assertFalse(self.ui.ui.password_assistance.isVisible()) self.patch(self.ui, 'name_assistance', self._set_called) @@ -174,7 +175,6 @@ """Test on_user_registered method.""" email = 'email@example' self.ui.ui.email_edit.setText(email) - self.assert_signal_emitted(self.ui.userRegistered, (email,), self.ui.on_user_registered, self.app_name, email) @@ -193,8 +193,6 @@ def test_initialize_page(self): """Widgets are properly initialized.""" self.ui.initializePage() - self.ui.show() - self.addCleanup(self.ui.hide) # set up account button expected = [QtGui.QWizard.BackButton, QtGui.QWizard.Stretch, @@ -210,12 +208,15 @@ self.assertFalse(self.ui.captcha_received) # labels - self.assertEqual(self.ui.ui.name_label.text(), gui.NAME_ENTRY) - self.assertEqual(self.ui.ui.email_label.text(), gui.EMAIL) - self.assertEqual(self.ui.ui.confirm_email_label.text(), + self.assertEqual(unicode(self.ui.ui.name_label.text()), + gui.NAME_ENTRY) + self.assertEqual(unicode(self.ui.ui.email_label.text()), + gui.EMAIL) + self.assertEqual(unicode(self.ui.ui.confirm_email_label.text()), gui.RETYPE_EMAIL) - self.assertEqual(self.ui.ui.password_label.text(), gui.PASSWORD) - self.assertEqual(self.ui.ui.confirm_password_label.text(), + self.assertEqual(unicode(self.ui.ui.password_label.text()), + gui.PASSWORD) + self.assertEqual(unicode(self.ui.ui.confirm_password_label.text()), gui.RETYPE_PASSWORD) # assistants @@ -230,8 +231,6 @@ self.patch(self.ui, 'set_next_validation', self._set_called) self.ui.initializePage() self.ui.captcha_received = True - self.ui.show() - self.addCleanup(self.ui.hide) self.ui.set_up_button.clicked.emit(False) self.assertEqual(self._called, ((False,), {})) @@ -239,13 +238,10 @@ def test_set_error_message(self): """Check the state of the label after calling: set_error_message.""" self.ui.email_assistance() - self.ui.show() - self.addCleanup(self.ui.hide) self.ui.set_error_message(self.ui.ui.email_assistance, "message") self.assertTrue(self.ui.ui.email_assistance.isVisible()) - self.assertEqual( - unicode(self.ui.ui.email_assistance.text()), - gui.ERROR_STYLE % "message") + self.assert_error_correct(self.ui.ui.email_assistance, "message", + max_width=self.ui.header.max_title_width) def test_blank_name(self): """Status when the name field is blank (spaces). @@ -255,13 +251,9 @@ """ self.ui.ui.name_edit.setText(" ") self.ui.name_assistance() - self.ui.show() - self.addCleanup(self.ui.hide) self.assertTrue(self.ui.ui.name_assistance.isVisible()) - self.assertEqual( - unicode(self.ui.ui.name_assistance.text()), - gui.ERROR_STYLE % gui.EMPTY_NAME) - self.ui.hide() + self.assert_error_correct(self.ui.ui.name_assistance, gui.EMPTY_NAME, + max_width=self.ui.header.max_title_width) def test_valid_name(self): """Status when the name field is valid. @@ -270,10 +262,7 @@ """ self.ui.ui.name_edit.setText("John Doe") self.ui.name_assistance() - self.ui.show() - self.addCleanup(self.ui.hide) self.assertFalse(self.ui.ui.name_assistance.isVisible()) - self.ui.hide() def test_invalid_email(self): """Status when the email field has no @. @@ -283,12 +272,10 @@ """ self.ui.ui.email_edit.setText("foobar") self.ui.email_assistance() - self.ui.show() - self.addCleanup(self.ui.hide) self.assertTrue(self.ui.ui.email_assistance.isVisible()) - self.assertEqual( - unicode(self.ui.ui.email_assistance.text()), - gui.ERROR_STYLE % gui.INVALID_EMAIL) + self.assert_error_correct(self.ui.ui.email_assistance, + gui.INVALID_EMAIL, + max_width=self.ui.header.max_title_width) def test_valid_email(self): """Status when the email field has a @. @@ -297,10 +284,7 @@ """ self.ui.ui.email_edit.setText("foo@bar") self.ui.email_assistance() - self.ui.show() - self.addCleanup(self.ui.hide) self.assertFalse(self.ui.ui.email_assistance.isVisible()) - self.ui.hide() def test_matching_emails(self): """Status when the email fields match. @@ -310,10 +294,7 @@ self.ui.ui.email_edit.setText("foo@bar") self.ui.ui.confirm_email_edit.setText("foo@bar") self.ui.confirm_email_assistance() - self.ui.show() - self.addCleanup(self.ui.hide) self.assertFalse(self.ui.ui.confirm_email_assistance.isVisible()) - self.ui.hide() def test_not_matching_emails(self): """Status when the email fields don't match. @@ -324,18 +305,13 @@ self.ui.ui.email_edit.setText("foo@bar") self.ui.ui.confirm_email_edit.setText("foo@baz") self.ui.confirm_email_assistance() - self.ui.show() - self.addCleanup(self.ui.hide) self.assertTrue(self.ui.ui.confirm_email_assistance.isVisible()) - self.assertEqual( - unicode(self.ui.ui.confirm_email_assistance.text()), - gui.ERROR_STYLE % gui.EMAIL_MATCH) - self.ui.hide() + self.assert_error_correct(self.ui.ui.confirm_email_assistance, + gui.EMAIL_MATCH, + max_width=self.ui.header.max_title_width) def test_focus_changed_password_visibility(self): """Check visibility changes when focus_changed() is executed.""" - self.ui.show() - self.addCleanup(self.ui.hide) self.ui.focus_changed(None, self.ui.ui.password_edit) self.assertTrue(self.ui.ui.password_assistance.isVisible()) @@ -350,33 +326,41 @@ self.ui.showEvent(QtGui.QShowEvent()) self.ui.hideEvent(QtGui.QHideEvent()) - def test_on_captcha_refreshing(self): + def test_on_captcha_refreshing_visible(self): """Check the state of the overlay on captcha refreshing.""" + self.ui.hide_overlay() + self.assertEqual(self._overlay_show_counter, 0) + self.assertTrue(self.ui.isEnabled()) + self.ui.on_captcha_refreshing() + + self.assertFalse(self.ui.isEnabled()) + self.assertEqual(self._overlay_show_counter, 1) + + def test_on_captcha_refreshing_not_visible(self): + """Check the state of the overlay on captcha refreshing.""" + self.ui.hide_overlay() + self.assertEqual(self._overlay_show_counter, 0) self.assertTrue(self.ui.isEnabled()) - self.ui.captcha_received = True - self.ui.show() - self.addCleanup(self.ui.hide) + + self.ui.hide() + self.ui.on_captcha_refreshing() + self.assertEqual(self._overlay_show_counter, 0) self.assertTrue(self.ui.isEnabled()) - self.ui.on_captcha_refreshing() - self.assertFalse(self.ui.isEnabled()) - self.assertEqual(self._overlay_show_counter, 1) def test_on_captcha_refresh_complete(self): """Check the state of the overlay on captcha refreshing complete.""" self.assertEqual(self._overlay_hide_counter, 0) - self.assertTrue(self.ui.isEnabled()) + self.ui.on_captcha_refresh_complete() + self.assertEqual(self._overlay_hide_counter, 1) - self.assertTrue(self.ui.isEnabled()) def test_hide_error_on_refresh_captcha(self): """Test that the errors are hidden on refresh captcha.""" - self.ui.show() - self.addCleanup(self.ui.hide) - self.ui.show_error(self.app_name, 'error-message') + self.ui.show_error('error-message') self.ui.ui.refresh_label.linkActivated.emit('link') self.assertEqual(self.ui.form_errors_label.text(), ' ') diff -Nru ubuntu-sso-client-2.99.90/ubuntu_sso/qt/tests/test_ssl_dialog.py ubuntu-sso-client-2.99.91/ubuntu_sso/qt/tests/test_ssl_dialog.py --- ubuntu-sso-client-2.99.90/ubuntu_sso/qt/tests/test_ssl_dialog.py 2012-03-06 18:01:25.000000000 +0000 +++ ubuntu-sso-client-2.99.91/ubuntu_sso/qt/tests/test_ssl_dialog.py 2012-03-21 13:41:45.000000000 +0000 @@ -142,7 +142,8 @@ def test_set_expander(self): """Test that the expander is correctly set.""" - self.assertEqual(SSL_CERT_DETAILS, self.dialog.expander.text()) + self.assertEqual(SSL_CERT_DETAILS, + unicode(self.dialog.expander.text())) self.assertNotEqual(None, self.dialog.expander.content) self.assertEqual(2, self.dialog.ui.expander_layout.indexOf( self.dialog.expander)) diff -Nru ubuntu-sso-client-2.99.90/ubuntu_sso/qt/tests/test_sso_wizard_page.py ubuntu-sso-client-2.99.91/ubuntu_sso/qt/tests/test_sso_wizard_page.py --- ubuntu-sso-client-2.99.90/ubuntu_sso/qt/tests/test_sso_wizard_page.py 2012-03-06 18:01:25.000000000 +0000 +++ ubuntu-sso-client-2.99.91/ubuntu_sso/qt/tests/test_sso_wizard_page.py 2012-03-21 13:41:45.000000000 +0000 @@ -16,98 +16,137 @@ """Test the SSOWizardPage and related.""" -from twisted.internet import defer +from ubuntu_sso.qt import PREFERED_UI_SIZE, sso_wizard_page as gui +from ubuntu_sso.qt.tests import ( + APP_NAME, + BaseTestCase, + build_string_for_pixels, + PageBaseTestCase, +) -from ubuntu_sso.qt import PREFERED_UI_SIZE -from ubuntu_sso.qt.setup_account_page import SetupAccountPage -from ubuntu_sso.qt.sso_wizard_page import Header -from ubuntu_sso.qt.tests import BaseTestCase, PageBaseTestCase +MAX_WIDTH = 100 -class HeaderTest(BaseTestCase): + +class WizardHeaderTestCase(BaseTestCase): """Tests for injected Header in each Wizard Page.""" - @defer.inlineCallbacks - def setUp(self): - yield super(HeaderTest, self).setUp() - self.header = Header() + kwargs = dict(max_width=MAX_WIDTH) + ui_class = gui.WizardHeader + ui_wizard_class = None def test_label_state(self): """Check the title and subtitle properties.""" - self.assertTrue(self.header.title_label.wordWrap()) - self.assertTrue(self.header.subtitle_label.wordWrap()) - self.assertFalse(self.header.title_label.isVisible()) - self.assertFalse(self.header.subtitle_label.isVisible()) + self.assertTrue(self.ui.title_label.wordWrap()) + self.assertTrue(self.ui.subtitle_label.wordWrap()) + self.assertFalse(self.ui.title_label.isVisible()) + self.assertFalse(self.ui.subtitle_label.isVisible()) def test_set_title(self): """Check if set_title works ok, showing the widget if necessary.""" - self.header.set_title('title') - self.assertEqual(self.header.title_label.text(), 'title') - self.header.show() - self.assertTrue(self.header.title_label.isVisible()) - self.header.hide() + max_width = self.ui.max_title_width + text = build_string_for_pixels(self.ui.title_label, max_width) + + self.ui.set_title(text) + + self.assert_title_correct(self.ui.title_label, text, max_width) def test_set_elided_title(self): """Check if set_title adds the ellipsis when necessary.""" # add an extra letter so we ensure this needs to be trimmed - title = 'a' * int(PREFERED_UI_SIZE['width'] * 0.95) + 'a' - self.header.set_title(title) - self.assertEqual(self.header.title_label.toolTip(), title) - expected = unicode(self.header.title_label.text()) - self.assertTrue(expected.endswith(u'\u2026')) + max_width = self.ui.max_title_width + text = build_string_for_pixels(self.ui.title_label, max_width + 10) + + self.ui.set_title(text) + + self.assert_title_correct(self.ui.title_label, text, max_width) def test_set_empty_title(self): """Check if the widget is hidden for empty title.""" - self.header.set_title('') - self.assertFalse(self.header.title_label.isVisible()) + self.ui.set_title('') + + self.assertEqual(self.ui.title_label.toolTip(), '') + self.assertFalse(self.ui.title_label.isVisible()) def test_set_subtitle(self): """Check if set_subtitle works ok, showing the widget if necessary.""" - self.header.set_subtitle('subtitle') - self.assertEqual(self.header.subtitle_label.text(), 'subtitle') - self.header.show() - self.assertTrue(self.header.subtitle_label.isVisible()) - self.header.hide() + max_width = self.ui.max_subtitle_width + text = build_string_for_pixels(self.ui.subtitle_label, max_width) + + self.ui.set_subtitle(text) + + self.assert_subtitle_correct(self.ui.subtitle_label, text, max_width) def test_set_elided_subtitle(self): """Check if set_subtitle adds the ellipsis when necessary.""" - subtitle = 'a' * int(PREFERED_UI_SIZE['width'] * 0.95) + 'a' - self.header.set_subtitle(subtitle) - self.assertEqual(self.header.subtitle_label.toolTip(), subtitle) - expected = unicode(self.header.subtitle_label.text()) - self.assertTrue(expected.endswith(u'\u2026')) + max_width = self.ui.max_subtitle_width + text = build_string_for_pixels(self.ui.subtitle_label, max_width + 10) + + self.ui.set_subtitle(text) + + self.assert_subtitle_correct(self.ui.subtitle_label, text, max_width) def test_set_empty_subtitle(self): """Check if the widget is hidden for empty subtitle.""" - self.header.set_title('') - self.assertFalse(self.header.title_label.isVisible()) + self.ui.set_subtitle('') + + self.assertEqual(self.ui.subtitle_label.toolTip(), '') + self.assertFalse(self.ui.subtitle_label.isVisible()) -class SSOWizardPageTest(PageBaseTestCase): +class BaseWizardPageTestCase(PageBaseTestCase): """Tests for SSOWizardPage.""" - ui_class = SetupAccountPage + kwargs = {} + ui_class = gui.BaseWizardPage + + def test_max_width(self): + """The max_width is correct.""" + self.assertEqual(self.ui.max_width, 0) + + +class SSOWizardPageTestCase(BaseWizardPageTestCase): + + """Tests for SSOWizardPage.""" + + kwargs = dict(app_name=APP_NAME) + ui_class = gui.SSOWizardPage + + def test_max_width(self): + """The max_width is correct.""" + self.assertEqual(self.ui.max_width, PREFERED_UI_SIZE['width']) def test_show_error(self): """Test show_error with a normal length string.""" message = 'error-message' - self.ui.show_error(self.app_name, message) - self.assertEqual(self.ui.form_errors_label.toolTip(), message) - expected = unicode(self.ui.form_errors_label.text()) - self.assertEqual(expected, message) + self.ui.show_error(message) + + self.assert_error_correct(self.ui.form_errors_label, message, + self.ui.header.max_title_width) def test_show_error_long_text(self): """Test show_error with a long length string.""" - message = 'a' * int(PREFERED_UI_SIZE['width'] * 0.95) + 'a' - self.ui.show_error(self.app_name, message) - self.assertEqual(self.ui.form_errors_label.toolTip(), message) - expected = unicode(self.ui.form_errors_label.text()) - self.assertTrue(expected.endswith(u'\u2026')) + message = build_string_for_pixels(self.ui.form_errors_label, + self.ui.header.max_title_width + 10) + + self.ui.show_error(message) + self.assert_error_correct(self.ui.form_errors_label, message, + self.ui.header.max_title_width) def test_hide_error(self): """Test show_error with a long length string.""" - message = ' ' self.ui.hide_error() - self.assertEqual(self.ui.form_errors_label.text(), message) + + self.assertEqual(self.ui.form_errors_label.text(), ' ') + + def test_setup_page_with_failing_backend(self): + """Test how the ui react with an invalid backend.""" + self.patch(gui.main, "get_sso_client", lambda: None) + self.patch(self.ui, "show_error", self._set_called) + self.ui.setup_page() + reason = 'There was a problem trying to setup the page %r' % self.ui + expected = ((reason,), {}) + self.assertEqual(expected, self._called) + self.assertFalse(self.ui.isEnabled()) diff -Nru ubuntu-sso-client-2.99.90/ubuntu_sso/qt/tests/test_ubuntu_sso_wizard.py ubuntu-sso-client-2.99.91/ubuntu_sso/qt/tests/test_ubuntu_sso_wizard.py --- ubuntu-sso-client-2.99.90/ubuntu_sso/qt/tests/test_ubuntu_sso_wizard.py 2012-03-06 18:01:25.000000000 +0000 +++ ubuntu-sso-client-2.99.91/ubuntu_sso/qt/tests/test_ubuntu_sso_wizard.py 2012-03-21 13:41:45.000000000 +0000 @@ -19,6 +19,11 @@ from PyQt4 import QtGui from twisted.internet import defer +from ubuntu_sso.tests import ( + APP_NAME, + EMAIL, + PASSWORD, +) from ubuntu_sso.qt import PREFERED_UI_SIZE, ubuntu_sso_wizard from ubuntu_sso.qt.tests import ( BaseTestCase, @@ -86,6 +91,11 @@ finish_button.clicked.emit(True) self.assertEqual(self._called, ((None,), {})) + def test_window_title(self): + """Check the window title for the application.""" + title = unicode(self.ui.windowTitle()) + self.assertEqual(title, ubuntu_sso_wizard.WINDOW_TITLE) + class UbuntuSSOWizardTestCase(BaseTestCase): @@ -102,13 +112,9 @@ def test_window_size(self): """check the window size.""" - self.ui.show() - self.addCleanup(self.ui.hide) size = self.ui.size() - # pylint: disable=E1101 - self.assertGreaterEqual(size.height(), PREFERED_UI_SIZE['height']) - self.assertGreaterEqual(size.width(), PREFERED_UI_SIZE['width']) - # pylint: enable=E1101 + self.assertTrue(size.height() >= PREFERED_UI_SIZE['height']) + self.assertTrue(size.width() >= PREFERED_UI_SIZE['width']) def test_move_to_success_page_state(self): """Test _move_to_success_page method.""" @@ -123,6 +129,9 @@ def test_overlay_shows(self): """Test if the signals call the overlay.show properly.""" + # reset the counter + self.ui.overlay.show_counter = 0 + for page in self.ui._pages: page.show_overlay() @@ -130,6 +139,9 @@ def test_overlay_hides(self): """Test if the signals call the overlay.show properly.""" + # reset the counter + self.ui.overlay.show_counter = 0 + for page in self.ui._pages: page.hide_overlay() @@ -146,3 +158,21 @@ self.ui.reset_password.passwordChanged.emit('') expected = ((self.ui.current_user,), {}) self.assertEqual(expected, self._called) + + def test_email_verification_page_params_from_current_user(self): + """Tests that email_verification_page receives the proper params.""" + self.ui._next_id = self.ui.current_user_page_id + self.ui.next() + self.ui.current_user.ui.email_edit.setText(EMAIL) + self.ui.current_user.ui.password_edit.setText(PASSWORD) + self.ui.current_user.on_user_not_validated(APP_NAME, EMAIL) + self.assertEqual(EMAIL, self.ui.email_verification.email) + self.assertEqual(PASSWORD, self.ui.email_verification.password) + + def test_email_verification_page_params_from_setup(self): + """Tests that email_verification_page receives the proper params.""" + self.ui.setup_account.ui.email_edit.setText(EMAIL) + self.ui.setup_account.ui.password_edit.setText(PASSWORD) + self.ui.setup_account.on_user_registered(APP_NAME, {}) + self.assertEqual(EMAIL, self.ui.email_verification.email) + self.assertEqual(PASSWORD, self.ui.email_verification.password) diff -Nru ubuntu-sso-client-2.99.90/ubuntu_sso/qt/ubuntu_sso_wizard.py ubuntu-sso-client-2.99.91/ubuntu_sso/qt/ubuntu_sso_wizard.py --- ubuntu-sso-client-2.99.90/ubuntu_sso/qt/ubuntu_sso_wizard.py 2012-03-06 18:01:25.000000000 +0000 +++ ubuntu-sso-client-2.99.91/ubuntu_sso/qt/ubuntu_sso_wizard.py 2012-03-21 13:41:45.000000000 +0000 @@ -30,7 +30,7 @@ USER_SUCCESS, ) from ubuntu_sso.logger import setup_gui_logging -from ubuntu_sso.qt import PREFERED_UI_SIZE +from ubuntu_sso.qt import PREFERED_UI_SIZE, WINDOW_TITLE from ubuntu_sso.qt.current_user_sign_in_page import CurrentUserSignInPage from ubuntu_sso.qt.email_verification_page import EmailVerificationPage from ubuntu_sso.qt.error_page import ErrorPage @@ -141,6 +141,8 @@ def _go_back_to_page(self, page): """Move back until it reaches the 'page'.""" + logger.debug('Moving back from page: %s, to page: %s', + self.currentPage(), page) page_id = self._pages[page] visited_pages = self.visitedPages() for index in reversed(visited_pages): @@ -150,30 +152,42 @@ def _move_to_reset_password_page(self): """Move to the reset password page wizard.""" + logger.debug('Moving to ResetPasswordPage from: %s', + self.currentPage()) self._next_id = self.reset_password_page_id self.next() self._next_id = -1 - def _move_to_email_verification_page(self): + def _move_to_email_verification_page(self, email): """Move to the email verification page wizard.""" + logger.debug('Moving to EmailVerificationPage from: %s', + self.currentPage()) self._next_id = self.email_verification_page_id + self.email_verification.email = unicode(email) + self.email_verification.password = self.currentPage().password self.next() self._next_id = -1 def _move_to_setup_account_page(self): """Move to the setup account page wizard.""" + logger.debug('Moving to SetupAccountPage from: %s', + self.currentPage()) self._next_id = self.setup_account_page_id self.next() self._next_id = -1 def _move_to_login_page(self): """Move to the login page wizard.""" + logger.debug('Moving to CurrentUserSignInPage from: %s', + self.currentPage()) self._next_id = self.current_user_page_id self.next() self._next_id = -1 def _move_to_success_page(self): """Move to the success page wizard.""" + logger.debug('Moving to SuccessPage from: %s', + self.currentPage()) self._next_id = self.success_page_id self.next() self.setButtonLayout([ @@ -186,6 +200,8 @@ def _move_to_forgotten_page(self): """Move to the forgotten page wizard.""" + logger.debug('Moving to ForgottenPasswordPage from: %s', + self.currentPage()) self._next_id = self.forgotten_password_page_id self.next() self._next_id = -1 @@ -258,6 +274,7 @@ logger.debug('UbuntuSSOClientGUI: app_name %r, kwargs %r.', app_name, kwargs) self.app_name = app_name + self.setWindowTitle(WINDOW_TITLE) # create the controller and the ui, then set the cb and call the show # method so that we can work self.wizard = UbuntuSSOWizard(app_name=app_name, **kwargs) diff -Nru ubuntu-sso-client-2.99.90/ubuntu_sso/utils/__init__.py ubuntu-sso-client-2.99.91/ubuntu_sso/utils/__init__.py --- ubuntu-sso-client-2.99.90/ubuntu_sso/utils/__init__.py 2012-03-06 18:01:25.000000000 +0000 +++ ubuntu-sso-client-2.99.91/ubuntu_sso/utils/__init__.py 2012-03-21 13:41:45.000000000 +0000 @@ -32,8 +32,14 @@ logger = setup_logging("ubuntu_sso.utils") -DATA_SUFFIX = 'data' BIN_SUFFIX = 'bin' +DATA_SUFFIX = 'data' + +if sys.platform == "win32": + from ubuntu_sso.utils import windows as source +else: + from ubuntu_sso.utils import linux as source +PLATFORM_QSS = source.PLATFORM_QSS def _get_dir(dir_name, dir_constant): @@ -88,8 +94,15 @@ found, return the value of the BIN_DIR. """ - result = _get_dir(dir_name=BIN_SUFFIX, dir_constant='BIN_DIR') + # If sys is frozen, this is an .exe, and all binaries are in + # the same place + if getattr(sys, "frozen", None) is not None: + exec_path = os.path.abspath(sys.executable) + result = os.path.dirname(exec_path) + else: + result = _get_dir(dir_name=BIN_SUFFIX, dir_constant='BIN_DIR') assert result is not None, '%r dir can not be None.' % BIN_SUFFIX + logger.info('get_bin_dir: returning dir located at %r.', result) return result diff -Nru ubuntu-sso-client-2.99.90/ubuntu_sso/utils/linux.py ubuntu-sso-client-2.99.91/ubuntu_sso/utils/linux.py --- ubuntu-sso-client-2.99.90/ubuntu_sso/utils/linux.py 1970-01-01 00:00:00.000000000 +0000 +++ ubuntu-sso-client-2.99.91/ubuntu_sso/utils/linux.py 2012-03-21 13:41:45.000000000 +0000 @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2010-2012 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify it +# under the terms of the GNU General Public License version 3, as published +# by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranties of +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR +# PURPOSE. See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program. If not, see . + +"""Platform specific constants and functions (for Linux).""" + +PLATFORM_QSS = ":/linux.qss" diff -Nru ubuntu-sso-client-2.99.90/ubuntu_sso/utils/runner/glib.py ubuntu-sso-client-2.99.91/ubuntu_sso/utils/runner/glib.py --- ubuntu-sso-client-2.99.90/ubuntu_sso/utils/runner/glib.py 2012-03-06 18:01:25.000000000 +0000 +++ ubuntu-sso-client-2.99.91/ubuntu_sso/utils/runner/glib.py 2012-03-21 13:41:45.000000000 +0000 @@ -27,7 +27,7 @@ logger = setup_logging("ubuntu_sso.utils.runner.glib") -NO_SUCH_FILE_OR_DIR = 'No such file or directory' +NO_SUCH_FILE_OR_DIR = '[Errno 2]' def spawn_program(args, reply_handler, error_handler): diff -Nru ubuntu-sso-client-2.99.90/ubuntu_sso/utils/runner/tx.py ubuntu-sso-client-2.99.91/ubuntu_sso/utils/runner/tx.py --- ubuntu-sso-client-2.99.90/ubuntu_sso/utils/runner/tx.py 2012-03-06 18:01:25.000000000 +0000 +++ ubuntu-sso-client-2.99.91/ubuntu_sso/utils/runner/tx.py 2012-03-21 13:41:45.000000000 +0000 @@ -26,7 +26,7 @@ logger = setup_logging("ubuntu_sso.utils.runner.tx") -NO_SUCH_FILE_OR_DIR = 'OSError: [Errno 2] No such file or directory' +NO_SUCH_FILE_OR_DIR = 'OSError: [Errno 2]' EXE_EXT = '' diff -Nru ubuntu-sso-client-2.99.90/ubuntu_sso/utils/tests/test_common.py ubuntu-sso-client-2.99.91/ubuntu_sso/utils/tests/test_common.py --- ubuntu-sso-client-2.99.90/ubuntu_sso/utils/tests/test_common.py 2012-03-06 18:01:25.000000000 +0000 +++ ubuntu-sso-client-2.99.91/ubuntu_sso/utils/tests/test_common.py 2012-03-21 13:41:45.000000000 +0000 @@ -17,6 +17,7 @@ """Tests for the oauth_headers helper function.""" import logging +import os import sys import time @@ -146,13 +147,21 @@ self.assertEqual(expected, result) -class GetBinDirTestCase(TestCase): +class GetBinDirTestCase(GetProjectDirTestCase): """Test case for get_bin_dir when constants module is not defined.""" DIR_NAME = utils.BIN_SUFFIX DIR_CONSTANT = 'BIN_DIR' DIR_GETTER = 'get_bin_dir' + def test_frozen_binary(self): + """Test that frozen binaries return a valid path.""" + sys.frozen = True + self.addCleanup(delattr, sys, "frozen") + expected = os.path.dirname(os.path.abspath(sys.executable)) + result = self.get_dir() + self.assertEqual(expected, result) + class GetBinDirWithConstantsTestCase(GetProjectDirWithConstantsTestCase): """Test case for get_bin_dir when constants module is not defined.""" diff -Nru ubuntu-sso-client-2.99.90/ubuntu_sso/utils/ui.py ubuntu-sso-client-2.99.91/ubuntu_sso/utils/ui.py --- ubuntu-sso-client-2.99.90/ubuntu_sso/utils/ui.py 2012-03-06 18:01:25.000000000 +0000 +++ ubuntu-sso-client-2.99.91/ubuntu_sso/utils/ui.py 2012-03-21 13:41:45.000000000 +0000 @@ -43,7 +43,7 @@ CAPTCHA_RELOAD_TEXT = _('refresh') CAPTCHA_RELOAD_TOOLTIP = _('Reload') CAPTCHA_REQUIRED_ERROR = _('The captcha is a required field') -CLOSE_AND_SETUP_LATER = _('Close window and setup later') +CLOSE_AND_SETUP_LATER = _('Close window and set up later') CONGRATULATIONS = _("Congratulations, {app_name} is installed!") CONNECT_HELP_LABEL = _('To connect this computer to %(app_name)s enter your ' 'details below.') @@ -133,6 +133,12 @@ SSL_CERT_DETAILS = _('Certificate details') SSL_CONNECT_BUTTON = _('Connect') SSL_DETAILS_HELP = _('the details ssl certificate we are going to show.') +SSL_DETAILS_TEMPLATE = ('Organization:\t%(organization)s\n' + 'Common Name:\t%(common_name)s\n' + 'Locality Name:\t%(locality_name)s\n' + 'Unit:\t%(unit)s\n' + 'Country:\t%(country_name)s\n' + 'State or Province:\t%(state_name)s') SSL_DESCRIPTION = _('Open the SSL certificate UI.') SSL_DIALOG_TITLE = _('SSL Certificate Not Valid') SSL_DOMAIN_HELP = _('the domain whose ssl certificate we are going to show.') diff -Nru ubuntu-sso-client-2.99.90/ubuntu_sso/utils/webclient/common.py ubuntu-sso-client-2.99.91/ubuntu_sso/utils/webclient/common.py --- ubuntu-sso-client-2.99.90/ubuntu_sso/utils/webclient/common.py 2012-03-06 18:01:25.000000000 +0000 +++ ubuntu-sso-client-2.99.91/ubuntu_sso/utils/webclient/common.py 2012-03-21 13:41:45.000000000 +0000 @@ -17,14 +17,23 @@ import cgi import collections +import os from httplib2 import iri2uri from oauth import oauth from twisted.internet import defer from urlparse import urlparse +from ubuntu_sso import USER_SUCCESS, UI_PROXY_CREDS_DIALOG +from ubuntu_sso.logger import setup_logging +from ubuntu_sso.utils.runner import spawn_program +from ubuntu_sso.utils.ui import SSL_DETAILS_TEMPLATE from ubuntu_sso.utils.webclient.timestamp import TimestampChecker +SSL_DIALOG = 'ubuntu-sso-ssl-certificate-qt' + +logger = setup_logging("ubuntu_sso.utils.webclient.common") + class WebClientError(Exception): """An http error happened while calling the webservice.""" @@ -34,8 +43,12 @@ """The request ended with bad_request, unauthorized or forbidden.""" +class ProxyUnauthorizedError(WebClientError): + """Failure raised when there is an issue with the proxy auth.""" + + class Response(object): - """A reponse object.""" + """A response object.""" def __init__(self, content, headers=None): """Initialize this instance.""" @@ -77,10 +90,14 @@ timestamp_checker = None - def __init__(self, username=None, password=None, oauth_sign_plain=False): + def __init__(self, appname='', username=None, password=None, + oauth_sign_plain=False): """Initialize this instance.""" + self.appname = appname self.username = username self.password = password + self.proxy_username = None + self.proxy_password = None self.oauth_sign_plain = oauth_sign_plain def request(self, iri, method="GET", extra_headers=None, @@ -154,3 +171,94 @@ def shutdown(self): """Shut down all pending requests (if possible).""" + + @defer.inlineCallbacks + def _load_proxy_creds_from_keyring(self, domain): + """Load the proxy creds from the keyring.""" + from ubuntu_sso.keyring import Keyring + keyring = Keyring() + try: + creds = yield keyring.get_credentials(str(domain)) + logger.debug('Got credentials from keyring.') + except Exception, e: + logger.error('Error when retrieving the creds.') + raise WebClientError('Error when retrieving the creds.', e) + if creds is not None: + # if we are loading the same creds it means that we got the wrong + # ones + if (self.proxy_username == creds['username'] and + self.proxy_password == creds['password']): + defer.returnValue(False) + else: + self.proxy_username = creds['username'] + self.proxy_password = creds['password'] + defer.returnValue(True) + logger.debug('Proxy creds not in keyring.') + defer.returnValue(False) + + def _launch_proxy_creds_dialog(self, domain, retry): + """Launch the dialog used to get the creds.""" + from ubuntu_sso.utils import get_bin_dir + + bin_dir = get_bin_dir() + args = (os.path.join(bin_dir, UI_PROXY_CREDS_DIALOG), '--domain', + domain) + if retry: + args += ('--retry',) + return spawn_program(args) + + @defer.inlineCallbacks + def request_proxy_auth_credentials(self, domain, retry): + """Request the auth creds to the user.""" + if not retry: + if (self.proxy_username is not None + and self.proxy_password is not None): + logger.debug('Not retry and credentials are present.') + defer.returnValue(True) + else: + creds_loaded = yield self._load_proxy_creds_from_keyring( + domain) + if creds_loaded: + defer.returnValue(True) + + try: + return_code = yield self._launch_proxy_creds_dialog(domain, retry) + except Exception, e: + logger.error('Error when running external ui process.') + raise WebClientError('Error when running external ui process.', e) + + if return_code == USER_SUCCESS: + creds_loaded = yield self._load_proxy_creds_from_keyring(domain) + defer.returnValue(creds_loaded) + else: + logger.debug('Could not retrieve the credentials. Return code: %r', + return_code) + defer.returnValue(False) + + def format_ssl_details(self, details): + """Return a formatted string with the details.""" + return SSL_DETAILS_TEMPLATE % details + + def _launch_ssl_dialog(self, domain, details): + """Launch a dialog used to approve the ssl cert.""" + from ubuntu_sso.utils import get_bin_dir + + bin_dir = get_bin_dir() + args = (os.path.join(bin_dir, SSL_DIALOG), '--domain', domain, + '--details', details, + '--appname', self.appname) + return spawn_program(args) + + def _was_ssl_accepted(self, cert_details): + """Return if the cert was already accepted.""" + # TODO: Ensure that we look at pinned certs in a following branch + return False + + @defer.inlineCallbacks + def request_ssl_cert_approval(self, domain, details): + """Request the user for ssl approval.""" + if self._was_ssl_accepted(details): + defer.returnValue(True) + + return_code = yield self._launch_ssl_dialog(domain, details) + defer.returnValue(return_code == USER_SUCCESS) diff -Nru ubuntu-sso-client-2.99.90/ubuntu_sso/utils/webclient/gsettings.py ubuntu-sso-client-2.99.91/ubuntu_sso/utils/webclient/gsettings.py --- ubuntu-sso-client-2.99.90/ubuntu_sso/utils/webclient/gsettings.py 2012-03-06 18:01:25.000000000 +0000 +++ ubuntu-sso-client-2.99.91/ubuntu_sso/utils/webclient/gsettings.py 2012-03-21 13:41:45.000000000 +0000 @@ -31,6 +31,24 @@ return hostname, username, password +def parse_manual_proxy_settings(scheme, gsettings): + """Parse the settings for a given scheme.""" + host, username, pwd = parse_proxy_host(gsettings[scheme + ".host"]) + settings = { + "host": host, + "port": gsettings[scheme + ".port"], + } + if scheme == "http" and gsettings["http.use-authentication"]: + username = gsettings["http.authentication-user"] + pwd = gsettings["http.authentication-password"] + if username is not None and pwd is not None: + settings.update({ + "username": username, + "password": pwd, + }) + return settings + + def get_proxy_settings(): """Parse the proxy settings as returned by the gsettings executable.""" output = subprocess.check_output(GSETTINGS_CMDLINE.split()) @@ -56,20 +74,9 @@ if mode == "none": settings = {} elif mode == "manual": - # attempt to parse the host - host, username, pwd = parse_proxy_host(gsettings["http.host"]) - settings = { - "host": host, - "port": gsettings["http.port"], - } - if gsettings["http.use-authentication"]: - username = gsettings["http.authentication-user"] - pwd = gsettings["http.authentication-password"] - if username is not None and pwd is not None: - settings.update({ - "username": username, - "password": pwd, - }) + settings = {} + for scheme in ["http", "https"]: + settings[scheme] = parse_manual_proxy_settings(scheme, gsettings) else: # If mode is automatic the PAC javascript should be interpreted # on each request. That is out of scope so it's ignored for now diff -Nru ubuntu-sso-client-2.99.90/ubuntu_sso/utils/webclient/__init__.py ubuntu-sso-client-2.99.91/ubuntu_sso/utils/webclient/__init__.py --- ubuntu-sso-client-2.99.90/ubuntu_sso/utils/webclient/__init__.py 2012-03-06 18:01:25.000000000 +0000 +++ ubuntu-sso-client-2.99.91/ubuntu_sso/utils/webclient/__init__.py 2012-03-21 13:41:45.000000000 +0000 @@ -19,6 +19,7 @@ # pylint: disable=W0611 from ubuntu_sso.utils.webclient.common import ( + ProxyUnauthorizedError, UnauthorizedError, WebClientError, ) @@ -26,8 +27,23 @@ def is_qt4reactor_installed(): """Check if the qt4reactor is installed.""" - reactor = sys.modules.get("twisted.internet.reactor") - return reactor and getattr(reactor, "qApp", None) + result = False + + if not 'PyQt4' in sys.modules: + return result + + try: + from PyQt4.QtCore import QCoreApplication + from PyQt4.QtGui import QApplication + + # we could be running a process with or without ui, and those are diff + # apps. + result = (QCoreApplication.instance() is not None + or QApplication.instance() is not None) + except ImportError: + pass + + return result def webclient_module(): diff -Nru ubuntu-sso-client-2.99.90/ubuntu_sso/utils/webclient/libsoup.py ubuntu-sso-client-2.99.91/ubuntu_sso/utils/webclient/libsoup.py --- ubuntu-sso-client-2.99.90/ubuntu_sso/utils/webclient/libsoup.py 2012-03-06 18:01:25.000000000 +0000 +++ ubuntu-sso-client-2.99.91/ubuntu_sso/utils/webclient/libsoup.py 2012-03-21 13:41:45.000000000 +0000 @@ -19,10 +19,12 @@ from twisted.internet import defer +from ubuntu_sso.logger import setup_logging from ubuntu_sso.utils.webclient.common import ( BaseWebClient, HeaderDict, Response, + ProxyUnauthorizedError, UnauthorizedError, WebClientError, ) @@ -30,6 +32,8 @@ URI_ANONYMOUS_TEMPLATE = "http://{host}:{port}/" URI_USERNAME_TEMPLATE = "http://{username}:{password}@{host}:{port}/" +logger = setup_logging("ubuntu_sso.utils.webclient.libsoup") + class WebClient(BaseWebClient): """A webclient with a libsoup backend.""" @@ -41,11 +45,12 @@ from gi.repository import Soup, SoupGNOME self.soup = Soup self.session = Soup.SessionAsync() - self.session.add_feature_by_type(SoupGNOME.ProxyResolverGNOME) + self.session.add_feature(SoupGNOME.ProxyResolverGNOME()) self.session.connect("authenticate", self._on_authenticate) def _on_message(self, session, message, d): """Handle the result of an http message.""" + logger.debug('_on_message status code is %s', message.status_code) if message.status_code == httplib.OK: headers = HeaderDict() response_headers = message.get_property("response-headers") @@ -57,14 +62,51 @@ elif message.status_code == httplib.UNAUTHORIZED: e = UnauthorizedError(message.reason_phrase) d.errback(e) + elif message.status_code == httplib.PROXY_AUTHENTICATION_REQUIRED: + e = ProxyUnauthorizedError(message.reason_phrase) + d.errback(e) else: e = WebClientError(message.reason_phrase) d.errback(e) - def _on_authenticate(self, sesion, message, auth, retrying, data=None): + @defer.inlineCallbacks + def _on_authenticate(self, session, message, auth, retrying, data=None): """Handle the "authenticate" signal.""" - if not retrying and self.username and self.password: - auth.authenticate(self.username, self.password) + self.session.pause_message(message) + try: + logger.debug('_on_authenticate: message status code is %s', + message.status_code) + if not retrying and self.username and self.password: + auth.authenticate(self.username, self.password) + if auth.is_for_proxy(): + logger.debug('_on_authenticate auth is for proxy.') + got_creds = yield self.request_proxy_auth_credentials( + self.session.props.proxy_uri.host, + retrying) + if got_creds: + logger.debug('Got proxy credentials from user.') + auth.authenticate(self.proxy_username, self.proxy_password) + finally: + self.session.unpause_message(message) + + @defer.inlineCallbacks + def _on_proxy_authenticate(self, failure, iri, method="GET", + extra_headers=None, oauth_credentials=None, post_content=None): + """Deal with wrong settings.""" + failure.trap(ProxyUnauthorizedError) + logger.debug('Proxy settings are wrong.') + got_creds = yield self.request_proxy_auth_credentials( + self.session.props.proxy_uri.host, + True) + if got_creds: + settings = dict(host=self.session.props.proxy_uri.host, + port=self.session.props.proxy_uri.port, + username=self.proxy_username, + password=self.proxy_password) + self.force_use_proxy(settings) + response = yield self.request(iri, method, extra_headers, + oauth_credentials, post_content) + defer.returnValue(response) @defer.inlineCallbacks def request(self, iri, method="GET", extra_headers=None, @@ -92,6 +134,8 @@ message.request_body.append(post_content) self.session.queue_message(message, self._on_message, d) + d.addErrback(self._on_proxy_authenticate, iri, method, extra_headers, + oauth_credentials, post_content) response = yield d defer.returnValue(response) diff -Nru ubuntu-sso-client-2.99.90/ubuntu_sso/utils/webclient/qtnetwork.py ubuntu-sso-client-2.99.91/ubuntu_sso/utils/webclient/qtnetwork.py --- ubuntu-sso-client-2.99.90/ubuntu_sso/utils/webclient/qtnetwork.py 2012-03-06 18:01:25.000000000 +0000 +++ ubuntu-sso-client-2.99.91/ubuntu_sso/utils/webclient/qtnetwork.py 2012-03-21 13:41:45.000000000 +0000 @@ -30,29 +30,61 @@ QNetworkProxyFactory, QNetworkReply, QNetworkRequest, + QSslCertificate, ) from twisted.internet import defer +from ubuntu_sso.logger import setup_logging from ubuntu_sso.utils.webclient.common import ( BaseWebClient, HeaderDict, + ProxyUnauthorizedError, Response, UnauthorizedError, WebClientError, ) from ubuntu_sso.utils.webclient import gsettings +logger = setup_logging("ubuntu_sso.utils.webclient.qtnetwork") + + +def build_proxy(settings_groups): + """Create a QNetworkProxy from these settings.""" + proxy_groups = [ + ("socks", QNetworkProxy.Socks5Proxy), + ("https", QNetworkProxy.HttpProxy), + ("http", QNetworkProxy.HttpProxy), + ] + for group, proxy_type in proxy_groups: + if group not in settings_groups: + continue + settings = settings_groups[group] + if "host" in settings and "port" in settings: + return QNetworkProxy(proxy_type, + hostName=settings.get("host", ""), + port=settings.get("port", 0), + user=settings.get("username", ""), + password=settings.get("password", "")) + logger.error("No proxy correctly configured.") + return QNetworkProxy(QNetworkProxy.DefaultProxy) + class WebClient(BaseWebClient): """A webclient with a qtnetwork backend.""" + proxy_instance = None + def __init__(self, *args, **kwargs): """Initialize this instance.""" super(WebClient, self).__init__(*args, **kwargs) self.nam = QNetworkAccessManager(QCoreApplication.instance()) self.nam.finished.connect(self._handle_finished) self.nam.authenticationRequired.connect(self._handle_authentication) + self.nam.proxyAuthenticationRequired.connect(self.handle_proxy_auth) + # Disabled until we make this a per-instance option + #self.nam.sslErrors.connect(self._handle_ssl_errors) self.replies = {} + self.proxy_retry = False self.setup_proxy() def setup_proxy(self): @@ -60,11 +92,43 @@ # QtNetwork knows how to use the system settings on both Win and Mac if sys.platform.startswith("linux"): settings = gsettings.get_proxy_settings() - if settings: - self.force_use_proxy(settings) + enabled = len(settings) > 0 + if enabled and WebClient.proxy_instance is None: + proxy = build_proxy(settings) + QNetworkProxy.setApplicationProxy(proxy) + WebClient.proxy_instance = proxy + elif enabled and WebClient.proxy_instance: + logger.info("Proxy already in use.") + else: + logger.info("Proxy is disabled.") else: QNetworkProxyFactory.setUseSystemConfiguration(True) + def handle_proxy_auth(self, proxy, authenticator): + """Proxy authentication is required.""" + logger.info("auth_required %r, %r", self.proxy_username, + proxy.hostName()) + if (self.proxy_username is not None and + self.proxy_username != str(authenticator.user())): + authenticator.setUser(self.proxy_username) + WebClient.proxy_instance.setUser(self.proxy_username) + if (self.proxy_password is not None and + self.proxy_password != str(authenticator.password())): + authenticator.setPassword(self.proxy_password) + WebClient.proxy_instance.setPassword(self.proxy_password) + + def _perform_request(self, request, method, post_buffer): + """Return a deferred that will be fired with a Response object.""" + d = defer.Deferred() + if method == "GET": + reply = self.nam.get(request) + elif method == "HEAD": + reply = self.nam.head(request) + else: + reply = self.nam.sendCustomRequest(request, method, post_buffer) + self.replies[reply] = d + return d + @defer.inlineCallbacks def request(self, iri, method="GET", extra_headers=None, oauth_credentials=None, post_content=None): @@ -86,23 +150,30 @@ for key, value in headers.iteritems(): request.setRawHeader(key, value) - d = defer.Deferred() - if method == "GET": - reply = self.nam.get(request) - elif method == "HEAD": - reply = self.nam.head(request) - else: - post_buffer = QBuffer() - post_buffer.setData(post_content) - reply = self.nam.sendCustomRequest(request, method, post_buffer) - self.replies[reply] = d - result = yield d + post_buffer = QBuffer() + post_buffer.setData(post_content) + try: + result = yield self._perform_request(request, method, post_buffer) + except ProxyUnauthorizedError, e: + app_proxy = QNetworkProxy.applicationProxy() + proxy_host = app_proxy.hostName() if app_proxy else "proxy server" + got_creds = yield self.request_proxy_auth_credentials( + proxy_host, self.proxy_retry) + if got_creds: + self.proxy_retry = True + result = yield self.request(iri, method, extra_headers, + oauth_credentials, post_content) + else: + excp = WebClientError('Proxy creds needed.', e) + defer.returnValue(excp) defer.returnValue(result) def _handle_authentication(self, reply, authenticator): """The reply needs authentication.""" - authenticator.setUser(self.username) - authenticator.setPassword(self.password) + if authenticator.user() != self.username: + authenticator.setUser(self.username) + if authenticator.password() != self.password: + authenticator.setPassword(self.password) def _handle_finished(self, reply): """The reply has finished processing.""" @@ -118,20 +189,51 @@ d.callback(response) else: error_string = reply.errorString() + logger.debug('_handle_finished error (%s,%s).', error, + error_string) if error == QNetworkReply.AuthenticationRequiredError: exception = UnauthorizedError(error_string, content) + elif error == QNetworkReply.ProxyAuthenticationRequiredError: + # we are going thru a proxy and we did not auth + exception = ProxyUnauthorizedError(error_string, content) else: exception = WebClientError(error_string, content) d.errback(exception) - def force_use_proxy(self, settings): + def _get_certificate_details(self, cert): + """Return an string with the details of the certificate.""" + detail_titles = {QSslCertificate.Organization: 'organization', + QSslCertificate.CommonName: 'common_name', + QSslCertificate.LocalityName: 'locality_name', + QSslCertificate.OrganizationalUnitName: 'unit', + QSslCertificate.CountryName: 'country_name', + QSslCertificate.StateOrProvinceName: 'state_name'} + details = {} + for info, title in detail_titles.iteritems(): + details[title] = str(cert.issuerInfo(info)) + return self.format_ssl_details(details) + + def _get_certificate_host(self, cert): + """Return the host of the cert.""" + return str(cert.issuerInfo(QSslCertificate.CommonName)) + + @defer.inlineCallbacks + def _handle_ssl_errors(self, reply, errors): + """Handle the case in which we got an ssl error.""" + # ask the user if the cer should be trusted + cert = errors[0].certificate() + trust_cert = yield self.request_ssl_cert_approval( + self._get_certificate_host(cert), + self._get_certificate_details(cert)) + if trust_cert: + reply.ignoreSslErrors() + + def force_use_proxy(self, https_settings): """Setup this webclient to use the given proxy settings.""" - proxy = QNetworkProxy(QNetworkProxy.HttpProxy, - hostName=settings.get("host", ""), - port=settings.get("port", 0), - user=settings.get("username", ""), - password=settings.get("password", "")) - self.nam.setProxy(proxy) + settings = {"https": https_settings} + proxy = build_proxy(settings) + QNetworkProxy.setApplicationProxy(proxy) + WebClient.proxy_instance = proxy def shutdown(self): """Shut down all pending requests (if possible).""" diff -Nru ubuntu-sso-client-2.99.90/ubuntu_sso/utils/webclient/tests/__init__.py ubuntu-sso-client-2.99.91/ubuntu_sso/utils/webclient/tests/__init__.py --- ubuntu-sso-client-2.99.90/ubuntu_sso/utils/webclient/tests/__init__.py 2012-03-06 18:01:25.000000000 +0000 +++ ubuntu-sso-client-2.99.91/ubuntu_sso/utils/webclient/tests/__init__.py 2012-03-21 13:41:45.000000000 +0000 @@ -17,9 +17,48 @@ """Tests for the proxy-aware webclient.""" from twisted.application import internet, service +from twisted.internet import ssl from twisted.web import http, server +# Some settings are not used as described in: +# https://bugzilla.gnome.org/show_bug.cgi?id=648237 + +TEMPLATE_GSETTINGS_OUTPUT = """\ +org.gnome.system.proxy autoconfig-url '{autoconfig_url}' +org.gnome.system.proxy ignore-hosts {ignore_hosts:s} +org.gnome.system.proxy mode '{mode}' +org.gnome.system.proxy.ftp host '{ftp_host}' +org.gnome.system.proxy.ftp port {ftp_port} +org.gnome.system.proxy.http authentication-password '{auth_password}' +org.gnome.system.proxy.http authentication-user '{auth_user}' +org.gnome.system.proxy.http host '{http_host}' +org.gnome.system.proxy.http port {http_port} +org.gnome.system.proxy.http use-authentication {http_use_auth} +org.gnome.system.proxy.https host '{https_host}' +org.gnome.system.proxy.https port {https_port} +org.gnome.system.proxy.socks host '{socks_host}' +org.gnome.system.proxy.socks port {socks_port} +""" + +BASE_GSETTINGS_VALUES = { + "autoconfig_url": "", + "ignore_hosts": ["localhost", "127.0.0.0/8"], + "mode": "none", + "ftp_host": "", + "ftp_port": 0, + "auth_password": "", + "auth_user": "", + "http_host": "", + "http_port": 0, + "http_use_auth": "false", + "https_host": "", + "https_port": 0, + "socks_host": "", + "socks_port": 0, +} + + class SaveHTTPChannel(http.HTTPChannel): """A save protocol to be used in tests.""" @@ -48,15 +87,26 @@ class BaseMockWebServer(object): """A mock webserver for testing""" - def __init__(self): + def __init__(self, ssl_settings=None): """Start up this instance.""" self.root = self.get_root_resource() self.site = SaveSite(self.root) application = service.Application('web') self.service_collection = service.IServiceCollection(application) #pylint: disable=E1101 - self.tcpserver = internet.TCPServer(0, self.site) - self.tcpserver.setServiceParent(self.service_collection) + ssl_context = None + if (ssl_settings is not None + and 'key' in ssl_settings + and 'cert' in ssl_settings): + ssl_context = ssl.DefaultOpenSSLContextFactory(ssl_settings['key'], + ssl_settings['cert']) + self.ssl_server = internet.SSLServer(0, self.site, ssl_context) + else: + self.ssl_server = None + self.server = internet.TCPServer(0, self.site) + self.server.setServiceParent(self.service_collection) + if self.ssl_server: + self.ssl_server.setServiceParent(self.service_collection) self.service_collection.startService() def get_root_resource(self): @@ -65,9 +115,27 @@ def get_iri(self): """Build the iri for this mock server.""" - #pylint: disable=W0212 - port_num = self.tcpserver._port.getHost().port - return u"http://127.0.0.1:%d/" % port_num + url = u"http://127.0.0.1:%d/" + return url % self.get_port() + + def get_ssl_iri(self): + """Build the ssl iri for this mock server.""" + if self.ssl_server: + url = u"https://127.0.0.1:%d/" + return url % self.get_ssl_port() + + def get_port(self): + """Return the port where we are listening.""" + # pylint: disable=W0212 + return self.server._port.getHost().port + # pylint: enable=W0212 + + def get_ssl_port(self): + """Return the ssl port where we are listening.""" + # pylint: disable=W0212 + if self.ssl_server: + return self.ssl_server._port.getHost().port + # pylint: enable=W0212 def stop(self): """Shut it down.""" diff -Nru ubuntu-sso-client-2.99.90/ubuntu_sso/utils/webclient/tests/test_gsettings.py ubuntu-sso-client-2.99.91/ubuntu_sso/utils/webclient/tests/test_gsettings.py --- ubuntu-sso-client-2.99.90/ubuntu_sso/utils/webclient/tests/test_gsettings.py 2012-03-06 18:01:25.000000000 +0000 +++ ubuntu-sso-client-2.99.91/ubuntu_sso/utils/webclient/tests/test_gsettings.py 2012-03-21 13:41:45.000000000 +0000 @@ -18,43 +18,10 @@ from twisted.trial.unittest import TestCase from ubuntu_sso.utils.webclient import gsettings - -# Some settings are not used as described in: -# https://bugzilla.gnome.org/show_bug.cgi?id=648237 - -TEMPLATE_GSETTINGS_OUTPUT = """\ -org.gnome.system.proxy autoconfig-url '{autoconfig_url}' -org.gnome.system.proxy ignore-hosts {ignore_hosts:s} -org.gnome.system.proxy mode '{mode}' -org.gnome.system.proxy.ftp host '{ftp_host}' -org.gnome.system.proxy.ftp port {ftp_port} -org.gnome.system.proxy.http authentication-password '{auth_password}' -org.gnome.system.proxy.http authentication-user '{auth_user}' -org.gnome.system.proxy.http host '{http_host}' -org.gnome.system.proxy.http port {http_port} -org.gnome.system.proxy.http use-authentication {http_use_auth} -org.gnome.system.proxy.https host '{https_host}' -org.gnome.system.proxy.https port {https_port} -org.gnome.system.proxy.socks host '{socks_host}' -org.gnome.system.proxy.socks port {socks_port} -""" - -BASE_GSETTINGS_VALUES = { - "autoconfig_url": "", - "ignore_hosts": ["localhost", "127.0.0.0/8"], - "mode": "none", - "ftp_host": "", - "ftp_port": 0, - "auth_password": "", - "auth_user": "", - "http_host": "", - "http_port": 0, - "http_use_auth": "false", - "https_host": "", - "https_port": 0, - "socks_host": "", - "socks_port": 0, -} +from ubuntu_sso.utils.webclient.tests import ( + BASE_GSETTINGS_VALUES, + TEMPLATE_GSETTINGS_OUTPUT, +) class ProxySettingsTestCase(TestCase): @@ -83,8 +50,8 @@ ps = gsettings.get_proxy_settings() self.assertEqual(ps, expected) - def test_gsettings_parser_http_anonymous(self): - """Test a parser of gsettings.""" + def _assert_parser_anonymous(self, scheme): + """Assert the parsing of anonymous settings.""" template_values = dict(BASE_GSETTINGS_VALUES) expected_host = "expected_host" expected_port = 54321 @@ -94,14 +61,22 @@ } template_values.update({ "mode": "manual", - "http_host": expected_host, - "http_port": expected_port, + scheme + "_host": expected_host, + scheme + "_port": expected_port, }) fake_output = TEMPLATE_GSETTINGS_OUTPUT.format(**template_values) self.patch(gsettings.subprocess, "check_output", lambda _: fake_output) ps = gsettings.get_proxy_settings() - self.assertEqual(ps, expected) + self.assertEqual(ps[scheme], expected) + + def test_gsettings_parser_http_anonymous(self): + """Test a parser of gsettings.""" + self._assert_parser_anonymous('http') + + def test_gsettings_parser_https_anonymus(self): + """Test a parser of gsettings.""" + self._assert_parser_anonymous('https') def test_gsettings_parser_http_authenticated(self): """Test a parser of gsettings.""" @@ -128,9 +103,9 @@ self.patch(gsettings.subprocess, "check_output", lambda _: fake_output) ps = gsettings.get_proxy_settings() - self.assertEqual(ps, expected) + self.assertEqual(ps["http"], expected) - def test_gsettings_parser_authenticated_url(self): + def _assert_parser_authenticated_url(self, scheme): """Test a parser of gsettings with creds in the url.""" template_values = dict(BASE_GSETTINGS_VALUES) expected_host = "expected_host" @@ -147,15 +122,23 @@ } template_values.update({ "mode": "manual", - "http_host": composed_url, - "http_port": expected_port, + scheme + "_host": composed_url, + scheme + "_port": expected_port, "http_use_auth": "false", }) fake_output = TEMPLATE_GSETTINGS_OUTPUT.format(**template_values) self.patch(gsettings.subprocess, "check_output", lambda _: fake_output) ps = gsettings.get_proxy_settings() - self.assertEqual(ps, expected) + self.assertEqual(ps[scheme], expected) + + def test_gsettings_parser_http_authenticated_url(self): + """Test a parser of gsettings with creds in the url.""" + self._assert_parser_authenticated_url('http') + + def test_gsettings_parser_https_authenticated_url(self): + """Test a parser of gsettings with creds in the url.""" + self._assert_parser_authenticated_url('https') def test_gsettings_auth_over_url(self): """Test that the settings are more important that the url.""" @@ -166,7 +149,7 @@ expected_password = "very secret password" composed_url = '%s:%s@%s' % ('user', 'random', expected_host) - expected = { + http_expected = { "host": expected_host, "port": expected_port, "username": expected_user, @@ -184,7 +167,7 @@ self.patch(gsettings.subprocess, "check_output", lambda _: fake_output) ps = gsettings.get_proxy_settings() - self.assertEqual(ps, expected) + self.assertEqual(ps["http"], http_expected) class ParseProxyHostTestCase(TestCase): diff -Nru ubuntu-sso-client-2.99.90/ubuntu_sso/utils/webclient/tests/test_webclient.py ubuntu-sso-client-2.99.91/ubuntu_sso/utils/webclient/tests/test_webclient.py --- ubuntu-sso-client-2.99.90/ubuntu_sso/utils/webclient/tests/test_webclient.py 2012-03-06 18:01:25.000000000 +0000 +++ ubuntu-sso-client-2.99.91/ubuntu_sso/utils/webclient/tests/test_webclient.py 2012-03-21 13:41:45.000000000 +0000 @@ -16,9 +16,12 @@ """Integration tests for the proxy-enabled webclient.""" import os +import shutil import sys import urllib +from OpenSSL import crypto +from socket import gethostname from twisted.cred import checkers, portal from twisted.internet import defer from twisted.web import guard, http, resource @@ -27,7 +30,15 @@ from ubuntuone.devtools.testcases import TestCase from ubuntuone.devtools.testcases.squid import SquidTestCase +from ubuntu_sso import ( + keyring, + EXCEPTION_RAISED, + USER_SUCCESS, + USER_CANCELLATION, +) from ubuntu_sso.utils import webclient +from ubuntu_sso.utils.ui import SSL_DETAILS_TEMPLATE +from ubuntu_sso.utils.webclient import gsettings, txweb from ubuntu_sso.utils.webclient.common import BaseWebClient, HeaderDict, oauth from ubuntu_sso.utils.webclient.tests import BaseMockWebServer @@ -188,9 +199,13 @@ return root -class FakeReactor(object): - """A fake reactor object.""" - qApp = "Sample qapp" +class FakeQApplication(object): + """A fake Qt module.""" + + @classmethod + def instance(cls): + """Return the instance.""" + return cls class ModuleSelectionTestCase(TestCase): @@ -201,10 +216,11 @@ self.patch(sys, "modules", {}) self.assertFalse(webclient.is_qt4reactor_installed()) - def test_is_qt4reactor_installed_installed(self): + def test_is_qt4reactor_installed_installed_core(self): """When the qt4reactor is installed, it returns true.""" - fake_sysmodules = {"twisted.internet.reactor": FakeReactor()} - self.patch(sys, "modules", fake_sysmodules) + from PyQt4 import QtCore + + self.patch(QtCore, 'QCoreApplication', FakeQApplication) self.assertTrue(webclient.is_qt4reactor_installed()) def assert_module_name(self, module, expected_name): @@ -230,6 +246,7 @@ """Test for the webclient.""" timeout = 1 + webclient_factory = webclient.webclient_factory @defer.inlineCallbacks def setUp(self): @@ -237,7 +254,7 @@ self.ws = MockWebServer() self.addCleanup(self.ws.stop) self.base_iri = self.ws.get_iri() - self.wc = webclient.webclient_factory() + self.wc = self.webclient_factory() self.addCleanup(self.wc.shutdown) @defer.inlineCallbacks @@ -306,13 +323,22 @@ @defer.inlineCallbacks def test_send_basic_auth(self): """The basic authentication headers are sent.""" - other_wc = webclient.webclient_factory(username=SAMPLE_USERNAME, - password=SAMPLE_PASSWORD) + other_wc = self.webclient_factory(username=SAMPLE_USERNAME, + password=SAMPLE_PASSWORD) self.addCleanup(other_wc.shutdown) result = yield other_wc.request(self.base_iri + GUARDED) self.assertEqual(SAMPLE_RESOURCE, result.content) @defer.inlineCallbacks + def test_send_basic_auth_wrong_credentials(self): + """Wrong credentials returns a webclient error.""" + other_wc = self.webclient_factory(username=SAMPLE_USERNAME, + password="wrong password!") + self.addCleanup(other_wc.shutdown) + yield self.assertFailure(other_wc.request(self.base_iri + GUARDED), + webclient.UnauthorizedError) + + @defer.inlineCallbacks def test_request_is_oauth_signed(self): """The request is oauth signed.""" tsc = self.wc.get_timestamp_checker() @@ -348,6 +374,57 @@ "The type of %r must be bytes" % result.content) +class FakeSavingReactor(object): + """A fake reactor that saves connection attempts.""" + + def __init__(self): + """Initialize this fake instance.""" + self.connections = [] + + def connectTCP(self, host, port, factory, *args): + """Fake the connection.""" + self.connections.append((host, port, args)) + factory.response_headers = {} + factory.deferred = defer.succeed("response content") + + def connectSSL(self, host, port, factory, *args): + """Fake the connection.""" + self.connections.append((host, port, args)) + factory.response_headers = {} + factory.deferred = defer.succeed("response content") + + +class TxWebClientTestCase(WebClientTestCase): + """Test case for txweb.""" + + webclient_factory = txweb.WebClient + + +class TxWebClientReactorReplaceableTestCase(TestCase): + """In the txweb client the reactor is replaceable.""" + + timeout = 3 + FAKE_HOST = u"fake" + FAKE_IRI_TEMPLATE = u"%%s://%s/fake_page" % FAKE_HOST + + @defer.inlineCallbacks + def _test_replaceable_reactor(self, iri): + """The reactor can be replaced with the tunnel client.""" + fake_reactor = FakeSavingReactor() + wc = txweb.WebClient(fake_reactor) + _response = yield wc.request(iri) + host, _port, _args = fake_reactor.connections[0] + self.assertEqual(host, self.FAKE_HOST) + + def test_replaceable_reactor_http(self): + """Test the replaceable reactor with an http iri.""" + return self._test_replaceable_reactor(self.FAKE_IRI_TEMPLATE % "http") + + def test_replaceable_reactor_https(self): + """Test the replaceable reactor with an https iri.""" + return self._test_replaceable_reactor(self.FAKE_IRI_TEMPLATE % "https") + + class TimestampCheckerTestCase(TestCase): """Tests for the timestampchecker classmethod.""" @@ -375,6 +452,8 @@ class BasicProxyTestCase(SquidTestCase): """Test that the proxy works at all.""" + timeout = 3 + @defer.inlineCallbacks def setUp(self): yield super(BasicProxyTestCase, self).setUp() @@ -404,10 +483,119 @@ result = yield self.wc.request(self.base_iri + SIMPLERESOURCE) self.assert_header_contains(result.headers["Via"], "squid") + @defer.inlineCallbacks + def test_auth_proxy_is_used_creds_requested(self): + """The authenticated proxy is used by the webclient.""" + settings = self.get_auth_proxy_settings() + partial_settings = dict(host=settings['host'], port=settings['port']) + + def fake_creds_request(domain, retry): + """Fake user interaction.""" + self.wc.proxy_username = settings['username'] + self.wc.proxy_password = settings['password'] + return defer.succeed(True) + + self.patch(self.wc, 'request_proxy_auth_credentials', + fake_creds_request) + + self.wc.force_use_proxy(partial_settings) + result = yield self.wc.request(self.base_iri + SIMPLERESOURCE) + self.assert_header_contains(result.headers["Via"], "squid") + + @defer.inlineCallbacks + def test_auth_proxy_is_requested_creds_bad_details(self): + """Test using wrong credentials with the proxy.""" + settings = self.get_auth_proxy_settings() + wrong_settings = dict(host=settings['host'], port=settings['port'], + username=settings['password'], + password=settings['username']) + + def fake_creds_request(domain, retry): + """Fake user interaction.""" + self.wc.proxy_username = settings['username'] + self.wc.proxy_password = settings['password'] + return defer.succeed(True) + + self.patch(self.wc, 'request_proxy_auth_credentials', + fake_creds_request) + + self.wc.force_use_proxy(wrong_settings) + result = yield self.wc.request(self.base_iri + SIMPLERESOURCE) + self.assert_header_contains(result.headers["Via"], "squid") + + @defer.inlineCallbacks + def test_auth_proxy_is_requested_creds_bad_details_user(self): + """Test using no creds and user providing the wrong ones.""" + settings = self.get_auth_proxy_settings() + partial_settings = dict(host=settings['host'], port=settings['port']) + + def fake_creds_request(domain, retry): + """Fake user interaction.""" + if retry: + self.wc.proxy_username = settings['username'] + self.wc.proxy_password = settings['password'] + else: + self.wc.proxy_username = settings['password'] + self.wc.proxy_password = settings['username'] + return defer.succeed(True) + + self.patch(self.wc, 'request_proxy_auth_credentials', + fake_creds_request) + + self.wc.force_use_proxy(partial_settings) + result = yield self.wc.request(self.base_iri + SIMPLERESOURCE) + self.assert_header_contains(result.headers["Via"], "squid") + + @defer.inlineCallbacks + def test_auth_proxy_is_requested_creds_bad_details_everywhere(self): + """Test when we pass the wrong settings and get the wrong settings.""" + settings = self.get_auth_proxy_settings() + wrong_settings = dict(host=settings['host'], port=settings['port'], + username=settings['password'], + password=settings['username']) + + def fake_creds_request(domain, retry): + """Fake user interaction.""" + if retry: + self.wc.proxy_username = settings['username'] + self.wc.proxy_password = settings['password'] + else: + self.wc.proxy_username = settings['password'] + self.wc.proxy_password = settings['username'] + return defer.succeed(True) + + self.patch(self.wc, 'request_proxy_auth_credentials', + fake_creds_request) + + self.wc.force_use_proxy(wrong_settings) + result = yield self.wc.request(self.base_iri + SIMPLERESOURCE) + self.assert_header_contains(result.headers["Via"], "squid") + + def test_auth_proxy_is_requested_user_cancels(self): + """Test when the user cancels the creds dialog.""" + settings = self.get_auth_proxy_settings() + partial_settings = dict(host=settings['host'], port=settings['port']) + + def fake_creds_request(domain, retry): + """Fake user interaction.""" + return defer.succeed(False) + + self.patch(self.wc, 'request_proxy_auth_credentials', + fake_creds_request) + + self.wc.force_use_proxy(partial_settings) + self.failUnlessFailure(self.wc.request(self.base_iri + SIMPLERESOURCE), + webclient.WebClientError) + if WEBCLIENT_MODULE_NAME.endswith(".txweb"): reason = "txweb does not support proxies." test_anonymous_proxy_is_used.skip = reason - test_authenticated_proxy_is_used.skip = reason + test_authenticated_proxy_is_used.kip = reason + test_auth_proxy_is_used_creds_requested.skip = reason + test_auth_proxy_is_requested_creds_bad_details.skip = reason + test_auth_proxy_is_requested_creds_bad_details_user.skip = reason + test_auth_proxy_is_requested_creds_bad_details_everywhere.skip = reason + test_auth_proxy_is_requested_user_cancels.skip = reason class HeaderDictTestCase(TestCase): @@ -562,3 +750,315 @@ """Test for the oauth signing code using HMAC-SHA1.""" oauth_sign = "HMAC-SHA1" + + +class FakeKeyring(object): + """A fake keyring.""" + + def __init__(self, creds): + """A fake keyring.""" + self.creds = creds + + def __call__(self): + """Fake instance callable.""" + return self + + def get_credentials(self, domain): + """A fake get_credentials.""" + if isinstance(self.creds, Exception): + return defer.fail(self.creds) + return defer.succeed(self.creds) + + +class RequestProxyAuthTestCase(TestCase): + """Test the spawn of the creds dialog.""" + + @defer.inlineCallbacks + def setUp(self): + """Set the different tests.""" + yield super(RequestProxyAuthTestCase, self).setUp() + self.wc = webclient.webclient_factory() + self.addCleanup(self.wc.shutdown) + self.domain = 'domain' + self.retry = False + self.creds = dict(username='username', password='password') + + self.keyring = FakeKeyring(self.creds) + self.patch(keyring, 'Keyring', self.keyring) + + self.spawn_return_code = USER_SUCCESS + + def fake_spawn_process(args): + """Fake spawning a process.""" + if isinstance(self.spawn_return_code, Exception): + return defer.fail(self.spawn_return_code) + return defer.succeed(self.spawn_return_code) + + self.patch(webclient.common, 'spawn_program', fake_spawn_process) + + def test_spawn_error(self): + """Test the case when we cannot spawn the process.""" + self.spawn_return_code = Exception() + self.failUnlessFailure(self.wc.request_proxy_auth_credentials( + self.domain, True), + webclient.WebClientError) + + @defer.inlineCallbacks + def test_creds_acquired(self): + """Test the case in which we do get the creds.""" + got_creds = yield self.wc.request_proxy_auth_credentials(self.domain, + self.retry) + self.assertTrue(got_creds, 'Return true when creds are present.') + self.assertEqual(self.wc.proxy_username, self.creds['username']) + self.assertEqual(self.wc.proxy_password, self.creds['password']) + + def test_creds_acquired_keyring_error(self): + """Test the case in which we cannot access the keyring.""" + self.keyring.creds = Exception() + self.failUnlessFailure(self.wc.request_proxy_auth_credentials( + self.domain, self.retry), + webclient.WebClientError) + + @defer.inlineCallbacks + def test_creds_none(self): + """Test the case in which we got None from the keyring.""" + self.keyring.creds = None + got_creds = yield self.wc.request_proxy_auth_credentials(self.domain, + self.retry) + self.assertFalse(got_creds, 'Return false when creds are not present.') + + def test_user_cancelation(self): + """Test the case in which the user cancels.""" + self.spawn_return_code = USER_CANCELLATION + got_creds = yield self.wc.request_proxy_auth_credentials(self.domain, + self.retry) + self.assertFalse(got_creds, 'Return true when user cancels.') + + def test_exception_error(self): + """Test the case in which something bad happened.""" + self.spawn_return_code = EXCEPTION_RAISED + got_creds = yield self.wc.request_proxy_auth_credentials(self.domain, + self.retry) + self.assertFalse(got_creds, 'Return true when user cancels.') + + +class BaseSSLTestCase(SquidTestCase): + """Base test that allows to use ssl connections.""" + + @defer.inlineCallbacks + def setUp(self): + """Set the diff tests.""" + yield super(BaseSSLTestCase, self).setUp() + self.cert_dir = os.path.join(self.tmpdir, 'cert') + self.cert_details = dict(organization='Canonical', + common_name=gethostname(), + locality_name='London', + unit='Ubuntu One', + country_name='UK', + state_name='London',) + self.ssl_settings = self._generate_self_signed_certificate( + self.cert_dir, + self.cert_details) + self.addCleanup(self._clean_ssl_certificate_files) + + self.ws = MockWebServer(self.ssl_settings) + self.addCleanup(self.ws.stop) + self.base_iri = self.ws.get_iri() + self.base_ssl_iri = self.ws.get_ssl_iri() + + def _clean_ssl_certificate_files(self): + """Remove the certificate files.""" + if os.path.exists(self.cert_dir): + shutil.rmtree(self.cert_dir) + + def _generate_self_signed_certificate(self, cert_dir, cert_details): + """Generate the required SSL certificates.""" + if not os.path.exists(cert_dir): + os.makedirs(cert_dir) + cert_path = os.path.join(cert_dir, 'cert.crt') + key_path = os.path.join(cert_dir, 'cert.key') + + if os.path.exists(cert_path): + os.unlink(cert_path) + if os.path.exists(key_path): + os.unlink(key_path) + + # create a key pair + key = crypto.PKey() + key.generate_key(crypto.TYPE_RSA, 1024) + + # create a self-signed cert + cert = crypto.X509() + cert.get_subject().C = cert_details['country_name'] + cert.get_subject().ST = cert_details['state_name'] + cert.get_subject().L = cert_details['locality_name'] + cert.get_subject().O = cert_details['organization'] + cert.get_subject().OU = cert_details['unit'] + cert.get_subject().CN = cert_details['common_name'] + cert.set_serial_number(1000) + cert.gmtime_adj_notBefore(0) + cert.gmtime_adj_notAfter(10 * 365 * 24 * 60 * 60) + cert.set_issuer(cert.get_subject()) + cert.set_pubkey(key) + cert.sign(key, 'sha1') + + with open(cert_path, 'wt') as fd: + fd.write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert)) + + with open(key_path, 'wt') as fd: + fd.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, key)) + + return dict(key=key_path, cert=cert_path) + + +class CorrectProxyTestCase(BaseSSLTestCase): + """Test the interaction with a SSL enabled proxy.""" + + @defer.inlineCallbacks + def setUp(self): + """Set the tests.""" + yield super(CorrectProxyTestCase, self).setUp() + + # fake the gsettings to have diff settings for https and http + http_settings = self.get_auth_proxy_settings() + + #remember so that we can use them in the creds request + proxy_username = http_settings['username'] + proxy_password = http_settings['password'] + + # delete the username and password so that we get a 407 for testing + del http_settings['username'] + del http_settings['password'] + + https_settings = self.get_nonauth_proxy_settings() + + proxy_settings = dict(http=http_settings, https=https_settings) + self.patch(gsettings, "get_proxy_settings", lambda: proxy_settings) + + self.wc = webclient.webclient_factory() + self.addCleanup(self.wc.shutdown) + + self.called = [] + + def fake_creds_request(domain, retry): + """Fake user interaction.""" + self.called.append('request_proxy_auth_credentials') + self.wc.proxy_username = proxy_username + self.wc.proxy_password = proxy_password + return defer.succeed(True) + + self.patch(self.wc, 'request_proxy_auth_credentials', + fake_creds_request) + + def assert_header_contains(self, headers, expected): + """One of the headers matching key must contain a given value.""" + self.assertTrue(any(expected in value for value in headers)) + + @defer.inlineCallbacks + def test_https_request(self): + """Test using the correct proxy for the ssl request. + + In order to assert that the correct proxy is used we expect not to call + the auth dialog since we set the https proxy not to use the auth proxy + and to fail because we are reaching a https page with bad self-signed + certs. + """ + # we fail due to the fake ssl cert + yield self.failUnlessFailure(self.wc.request( + self.base_ssl_iri + SIMPLERESOURCE), + webclient.WebClientError) + # https requests do not use the auth proxy therefore called should be + # empty. This asserts that we are using the correct settings for the + # request. + self.assertEqual([], self.called) + + @defer.inlineCallbacks + def test_http_request(self): + """Test using the correct proxy for the plain request. + + This tests does the opposite to the https tests. We did set the auth + proxy for the http request therefore we expect the proxy dialog to be + used and not to get an error since we are not visiting a https with bad + self-signed certs. + """ + # we do not fail since we are not going to the https page + result = yield self.wc.request(self.base_iri + SIMPLERESOURCE) + self.assert_header_contains(result.headers["Via"], "squid") + # assert that we did go through the auth proxy + self.assertIn('request_proxy_auth_credentials', self.called) + + if WEBCLIENT_MODULE_NAME.endswith(".txweb"): + reason = 'Multiple proxy settings is not supported.' + test_https_request.skip = reason + test_http_request.skip = reason + + if WEBCLIENT_MODULE_NAME.endswith(".libsoup"): + reason = 'Hard to test since we need to fully mock gsettings.' + test_https_request.skip = reason + test_http_request.skip = reason + + if WEBCLIENT_MODULE_NAME.endswith(".qtnetwork"): + reason = ('Updating proxy settings is not well support due to bug' + ' QTBUG-14850: https://bugreports.qt-project.org/' + 'browse/QTBUG-14850') + test_https_request.skip = reason + test_http_request.skip = reason + + +class SSLTestCase(BaseSSLTestCase): + """Test error handling when dealing with ssl.""" + + @defer.inlineCallbacks + def setUp(self): + """Set the diff tests.""" + yield super(SSLTestCase, self).setUp() + + self.wc = webclient.webclient_factory() + self.addCleanup(self.wc.shutdown) + + self.return_code = USER_CANCELLATION + self.called = [] + + def fake_launch_ssl_dialog(client, domain, details): + """Fake the ssl dialog.""" + self.called.append(('_launch_ssl_dialog', domain, details)) + return defer.succeed(self.return_code) + + self.patch(BaseWebClient, '_launch_ssl_dialog', fake_launch_ssl_dialog) + + @defer.inlineCallbacks + def _assert_ssl_fail_user_accepts(self, proxy_settings=None): + """Assert the dialog is shown in an ssl fail.""" + self.return_code = USER_SUCCESS + if proxy_settings: + self.wc.force_use_proxy(proxy_settings) + yield self.wc.request(self.base_ssl_iri + SIMPLERESOURCE) + details = SSL_DETAILS_TEMPLATE % self.cert_details + self.assertIn(('_launch_ssl_dialog', gethostname(), details), + self.called) + + def test_ssl_fail_dialog_user_accepts(self): + """Test showing the dialog and accepting.""" + self._assert_ssl_fail_user_accepts() + + def test_ssl_fail_dialog_user_accepts_via_proxy(self): + """Test showing the dialog and accepting when using a proxy.""" + self._assert_ssl_fail_user_accepts(self.get_nonauth_proxy_settings()) + + def test_ssl_fail_dialog_user_rejects(self): + """Test showing the dialog and rejecting.""" + self.failUnlessFailure(self.wc.request(self.base_iri + SIMPLERESOURCE), + webclient.WebClientError) + + def test_format_ssl_details(self): + """Assert that details are correctly formatted""" + details = SSL_DETAILS_TEMPLATE % self.cert_details + self.assertEqual(details, + self.wc.format_ssl_details(self.cert_details)) + + if (WEBCLIENT_MODULE_NAME.endswith(".txweb") or + WEBCLIENT_MODULE_NAME.endswith(".libsoup")): + reason = 'SSL support has not yet been implemented.' + test_ssl_fail_dialog_user_accepts.skip = reason + test_ssl_fail_dialog_user_accepts_via_proxy.skip = reason + test_ssl_fail_dialog_user_rejects.skip = reason diff -Nru ubuntu-sso-client-2.99.90/ubuntu_sso/utils/webclient/txweb.py ubuntu-sso-client-2.99.91/ubuntu_sso/utils/webclient/txweb.py --- ubuntu-sso-client-2.99.90/ubuntu_sso/utils/webclient/txweb.py 2012-03-06 18:01:25.000000000 +0000 +++ ubuntu-sso-client-2.99.91/ubuntu_sso/utils/webclient/txweb.py 2012-03-21 13:41:45.000000000 +0000 @@ -16,11 +16,9 @@ """A webclient backend that uses twisted.web.client.""" import base64 +import urlparse -from StringIO import StringIO - -from twisted.internet import defer, protocol -from zope.interface import implements +from twisted.internet import defer from ubuntu_sso.utils.webclient.common import ( BaseWebClient, @@ -31,64 +29,80 @@ ) -class StringProtocol(protocol.Protocol): - """Hold the stuff received in a StringIO.""" - - # pylint: disable=C0103 - def __init__(self): - """Initialize this instance.""" - self.deferred = defer.Deferred() - self.content = StringIO() - - def dataReceived(self, data): - """Some more blocks received.""" - self.content.write(data) - - def connectionLost(self, reason=protocol.connectionDone): - """No more bytes available.""" - self.deferred.callback(self.content.getvalue()) - - -class StringProducer(object): - """Simple implementation of IBodyProducer.""" +class RawResponse(object): + """A raw response from the webcall.""" - # delay import, otherwise a default reactor gets installed - from twisted.web import iweb + def __init__(self, headers, content, code=200, phrase="OK"): + """Initialize this response.""" + self.headers = headers + self.content = content + self.code = code + self.phrase = phrase - implements(iweb.IBodyProducer) - def __init__(self, body): - """Initialize this instance with some bytes.""" - self.body = body - self.length = len(body) - - # pylint: disable=C0103 - def startProducing(self, consumer): - """Start producing to the given IConsumer provider.""" - consumer.write(self.body) - return defer.succeed(None) - - def pauseProducing(self): - """In our case, do nothing.""" +class WebClient(BaseWebClient): + """A simple web client that does not support proxies, yet.""" - def stopProducing(self): - """In our case, do nothing.""" + def __init__(self, connector=None, context_factory=None, **kwargs): + """Initialize this webclient.""" + super(WebClient, self).__init__(**kwargs) + + if connector is None: + from twisted.internet import reactor + self.connector = reactor + else: + self.connector = connector + if context_factory is None: + from twisted.internet import ssl + self.context_factory = ssl.ClientContextFactory() + else: + self.context_factory = context_factory -class WebClient(BaseWebClient): - """A simple web client that does not support proxies, yet.""" + @defer.inlineCallbacks + def raw_request(self, method, uri, headers, postdata): + """Make a raw http request.""" + # delay import, otherwise a default reactor gets installed + from twisted.web import client, error + + parsed_url = urlparse.urlparse(uri) + + # pylint: disable=E1101 + https = parsed_url.scheme == "https" + host = parsed_url.netloc.split(":")[0] + # pylint: enable=E1101 + if parsed_url.port is None: + port = 443 if https else 80 + else: + port = parsed_url.port - # delay import, otherwise a default reactor gets installed - from twisted.internet import reactor - from twisted.web import client, http, http_headers + factory = client.HTTPClientFactory(uri, method=method, + postdata=postdata, + headers=headers, + followRedirect=False) + # pylint: disable=E1103 + if https: + self.connector.connectSSL(host, port, factory, + self.context_factory) + else: + self.connector.connectTCP(host, port, factory) + # pylint: enable=E1103 - # Undefined variable 'http_headers', 'client', 'reactor', 'http' - # pylint: disable=E0602 + try: + content = yield factory.deferred + response = RawResponse(factory.response_headers, content) + except error.Error as e: + response = RawResponse(factory.response_headers, e.response, + int(e.status), e.message) + defer.returnValue(response) @defer.inlineCallbacks def request(self, iri, method="GET", extra_headers=None, oauth_credentials=None, post_content=None): """Get the page, or fail trying.""" + # delay import, otherwise a default reactor gets installed + from twisted.web import http + uri = self.iri_to_uri(iri) if extra_headers: @@ -107,40 +121,25 @@ headers["Authorization"] = "Basic " + auth try: - request_headers = http_headers.Headers() - for key, value in headers.items(): - request_headers.addRawHeader(key, value) - agent = client.Agent(reactor) - if post_content: - body_producer = StringProducer(post_content) - else: - body_producer = None - agent_response = yield agent.request(method, uri, - headers=request_headers, - bodyProducer=body_producer) - raw_headers = agent_response.headers.getAllRawHeaders() - response_headers = HeaderDict(raw_headers) + raw_response = yield self.raw_request(method, uri, + headers=headers, + postdata=post_content) + response_headers = HeaderDict(raw_response.headers) if method.lower() != "head": - response_content = yield self.get_agent_content(agent_response) + response_content = raw_response.content else: response_content = "" - if agent_response.code == http.OK: + if raw_response.code == http.OK: defer.returnValue(Response(response_content, response_headers)) - if agent_response.code == http.UNAUTHORIZED: - raise UnauthorizedError(agent_response.phrase, + if raw_response.code == http.UNAUTHORIZED: + raise UnauthorizedError(raw_response.phrase, response_content) - raise WebClientError(agent_response.phrase, response_content) + raise WebClientError(raw_response.phrase, response_content) except WebClientError: raise except Exception as e: raise WebClientError(e.message, e) - def get_agent_content(self, agent_response): - """Get the contents of an agent response.""" - string_protocol = StringProtocol() - agent_response.deliverBody(string_protocol) - return string_protocol.deferred - def force_use_proxy(self, settings): """Setup this webclient to use the given proxy settings.""" - # No proxy support in twisted.web.client + # No direct proxy support in twisted.web.client diff -Nru ubuntu-sso-client-2.99.90/ubuntu_sso/utils/windows.py ubuntu-sso-client-2.99.91/ubuntu_sso/utils/windows.py --- ubuntu-sso-client-2.99.90/ubuntu_sso/utils/windows.py 1970-01-01 00:00:00.000000000 +0000 +++ ubuntu-sso-client-2.99.91/ubuntu_sso/utils/windows.py 2012-03-21 13:41:45.000000000 +0000 @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2010-2012 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify it +# under the terms of the GNU General Public License version 3, as published +# by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranties of +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR +# PURPOSE. See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program. If not, see . + +"""Platform specific constants and functions (for Windows).""" + +PLATFORM_QSS = ":/windows.qss"