diff options
| -rw-r--r-- | src/leap/gui/firstrun/login.py | 247 | ||||
| -rw-r--r-- | src/leap/gui/firstrun/register.py | 7 | ||||
| -rwxr-xr-x | src/leap/gui/firstrun/tests/integration/fake_provider.py | 31 | ||||
| -rw-r--r-- | src/leap/gui/tests/test_firstrun_login.py | 212 | ||||
| -rw-r--r-- | src/leap/gui/tests/test_firstrun_register.py | 20 | 
5 files changed, 377 insertions, 140 deletions
| diff --git a/src/leap/gui/firstrun/login.py b/src/leap/gui/firstrun/login.py index 02bace86..e7afee9f 100644 --- a/src/leap/gui/firstrun/login.py +++ b/src/leap/gui/firstrun/login.py @@ -82,6 +82,120 @@ class LogInPage(InlineValidationPage, UserFormMixIn):  # InlineValidationPage          #self.registerField('is_login_wizard') +    # actual checks + +    def _do_checks(self): + +        full_username = self.userNameLineEdit.text() +        ########################### +        # 0) check user@domain form +        ########################### + +        def checkusername(): +            if full_username.count('@') != 1: +                return self.fail( +                    self.tr( +                        "Username must be in the username@provider form.")) +            else: +                return True + +        yield(("head_sentinel", 0), checkusername) + +        username, domain = full_username.split('@') +        password = self.userPasswordLineEdit.text() + +        # We try a call to an authenticated +        # page here as a mean to catch +        # srp authentication errors while +        wizard = self.wizard() +        eipconfigchecker = wizard.eipconfigchecker() + +        ######################## +        # 1) try name resolution +        ######################## +        # show the frame before going on... +        QtCore.QMetaObject.invokeMethod( +            self, "showStepsFrame") + +        # Able to contact domain? +        # can get definition? +        # two-by-one +        def resolvedomain(): +            try: +                eipconfigchecker.fetch_definition(domain=domain) + +            # we're using requests here for all +            # the possible error cases that it catches. +            except requests.exceptions.ConnectionError as exc: +                return self.fail(exc.message[1]) +            except requests.exceptions.HTTPError as exc: +                return self.fail(exc.message) +            except Exception as exc: +                # XXX get catchall error msg +                return self.fail( +                    exc.message) +            else: +                return True + +        yield((self.tr("Resolving domain name"), 20), resolvedomain) + +        wizard.set_providerconfig( +            eipconfigchecker.defaultprovider.config) + +        ######################## +        # 2) do authentication +        ######################## +        credentials = username, password +        pCertChecker = wizard.providercertchecker( +            domain=domain) + +        def validate_credentials(): +            ################# +            # FIXME #BUG #638 +            verify = False + +            try: +                pCertChecker.download_new_client_cert( +                    credentials=credentials, +                    verify=verify) + +            except auth.SRPAuthenticationError as exc: +                return self.fail( +                    self.tr("Authentication error: %s" % exc.message)) + +            except Exception as exc: +                return self.fail(exc.message) + +            else: +                return True + +        yield(('Validating credentials', 60), validate_credentials) + +        self.set_done() +        yield(("end_sentinel", 100), lambda: None) + +    def green_validation_status(self): +        val = self.validationMsg +        val.setText(self.tr('Credentials validated.')) +        val.setStyleSheet(styles.GreenLineEdit) + +    def on_checks_validation_ready(self): +        """ +        after checks +        """ +        if self.is_done(): +            self.disableFields() +            self.cleanup_errormsg() +            self.clean_wizard_errors(self.current_page) +            # make the user confirm the transition +            # to next page. +            self.nextText('&Next') +            self.nextFocus() +            self.green_validation_status() +            self.do_confirm_next = True + +    # ui update +      def nextText(self, text):          self.setButtonText(              QtGui.QWizard.NextButton, text) @@ -94,12 +208,18 @@ class LogInPage(InlineValidationPage, UserFormMixIn):  # InlineValidationPage          self.wizard().button(              QtGui.QWizard.NextButton).setDisabled(True) -    def onUserNameEdit(self, *args): +    def onUserNamePositionChanged(self, *args):          if self.initial_username_sample:              self.userNameLineEdit.setText('')              # XXX set regular color              self.initial_username_sample = None +    def onUserNameTextChanged(self, *args): +        if self.initial_username_sample: +            k = args[0][-1] +            self.initial_username_sample = None +            self.userNameLineEdit.setText(k) +      def disableFields(self):          for field in (self.userNameLineEdit,                        self.userPasswordLineEdit): @@ -111,13 +231,8 @@ class LogInPage(InlineValidationPage, UserFormMixIn):  # InlineValidationPage          errors = self.wizard().get_validation_error(              self.current_page) -        #prev_er = getattr(self, 'prevalidation_error', None)          showerr = self.validationMsg.setText -        #if not errors and prev_er: -            #showerr(prev_er) -            #return -#          if errors:              bad_str = getattr(self, 'bad_string', None)              cur_str = self.userNameLineEdit.text() @@ -128,9 +243,6 @@ class LogInPage(InlineValidationPage, UserFormMixIn):  # InlineValidationPage                  self.bad_string = cur_str                  showerr(errors)              else: -                #if prev_er: -                    #showerr(prev_er) -                    #return                  # not the first time                  if cur_str == bad_str:                      showerr(errors) @@ -177,7 +289,9 @@ class LogInPage(InlineValidationPage, UserFormMixIn):  # InlineValidationPage          username = self.userNameLineEdit          username.setText('username@provider.example.org')          username.cursorPositionChanged.connect( -            self.onUserNameEdit) +            self.onUserNamePositionChanged) +        username.textChanged.connect( +            self.onUserNameTextChanged)          self.initial_username_sample = True          self.validationMsg.setText('')          self.valFrame.hide() @@ -215,116 +329,3 @@ class LogInPage(InlineValidationPage, UserFormMixIn):  # InlineValidationPage              self.do_checks()          return self.is_done() - -    def _do_checks(self): -        # XXX convert this to inline - -        full_username = self.userNameLineEdit.text() -        ########################### -        # 0) check user@domain form -        ########################### - -        def checkusername(): -            if full_username.count('@') != 1: -                return self.fail( -                    self.tr( -                        "Username must be in the username@provider form.")) -            else: -                return True - -        yield(("head_sentinel", 0), checkusername) - -        # XXX I think this is not needed -        # since we're also checking for the is_signup field. -        #self.wizard().from_login = True - -        username, domain = full_username.split('@') -        password = self.userPasswordLineEdit.text() - -        # We try a call to an authenticated -        # page here as a mean to catch -        # srp authentication errors while -        wizard = self.wizard() -        eipconfigchecker = wizard.eipconfigchecker() - -        ######################## -        # 1) try name resolution -        ######################## -        # show the frame before going on... -        QtCore.QMetaObject.invokeMethod( -            self, "showStepsFrame") - -        # Able to contact domain? -        # can get definition? -        # two-by-one -        def resolvedomain(): -            try: -                eipconfigchecker.fetch_definition(domain=domain) - -            # we're using requests here for all -            # the possible error cases that it catches. -            except requests.exceptions.ConnectionError as exc: -                return self.fail(exc.message[1]) -            except requests.exceptions.HTTPError as exc: -                return self.fail(exc.message) -            except Exception as exc: -                # XXX get catchall error msg -                return self.fail( -                    exc.message) - -        yield((self.tr("resolving domain name"), 20), resolvedomain) - -        wizard.set_providerconfig( -            eipconfigchecker.defaultprovider.config) - -        ######################## -        # 2) do authentication -        ######################## -        credentials = username, password -        pCertChecker = wizard.providercertchecker( -            domain=domain) - -        def validate_credentials(): -            ################# -            # FIXME #BUG #638 -            verify = False - -            try: -                pCertChecker.download_new_client_cert( -                    credentials=credentials, -                    verify=verify) - -            except auth.SRPAuthenticationError as exc: -                return self.fail( -                    self.tr("Authentication error: %s" % exc.message)) - -            except Exception as exc: -                return self.fail(exc.message) - -            else: -                return True - -        yield(('Validating credentials', 20), validate_credentials) - -        self.set_done() -        yield(("end_sentinel", 0), lambda: None) - -    def green_validation_status(self): -        val = self.validationMsg -        val.setText(self.tr('Credentials validated.')) -        val.setStyleSheet(styles.GreenLineEdit) - -    def on_checks_validation_ready(self): -        """ -        after checks -        """ -        if self.is_done(): -            self.disableFields() -            self.cleanup_errormsg() -            self.clean_wizard_errors(self.current_page) -            # make the user confirm the transition -            # to next page. -            self.nextText('&Next') -            self.nextFocus() -            self.green_validation_status() -            self.do_confirm_next = True diff --git a/src/leap/gui/firstrun/register.py b/src/leap/gui/firstrun/register.py index 7fd5c574..4c811093 100644 --- a/src/leap/gui/firstrun/register.py +++ b/src/leap/gui/firstrun/register.py @@ -363,9 +363,6 @@ class RegisterUserPage(InlineValidationPage, UserFormMixIn):          inits wizard page          """          provider = unicode(self.field('provider_domain')) -        # hack. don't get why I'm  getting a QVariant there, -        # making segfault in tests. -        provider = QtCore.QString(provider)          if provider:              # here we should have provider              # but in tests we might not. @@ -384,7 +381,7 @@ class RegisterUserPage(InlineValidationPage, UserFormMixIn):      def nextId(self):          wizard = self.wizard() -        if not wizard: -            return +        #if not wizard: +            #return          # XXX this should be called connect          return wizard.get_page_index('signupvalidation') diff --git a/src/leap/gui/firstrun/tests/integration/fake_provider.py b/src/leap/gui/firstrun/tests/integration/fake_provider.py index 33ee0ee6..445b4487 100755 --- a/src/leap/gui/firstrun/tests/integration/fake_provider.py +++ b/src/leap/gui/firstrun/tests/integration/fake_provider.py @@ -40,6 +40,8 @@ from twisted.web.static import File  from twisted.web.resource import Resource  from twisted.internet import reactor +from leap.testing.https_server import where +  # See  # http://twistedmatrix.com/documents/current/web/howto/web-in-60/index.htmln  # for more examples @@ -229,14 +231,13 @@ def get_certs_path():  def get_TLS_credentials():      # XXX this is giving errors      # XXX REview! We want to use gnutls! -    certs_path = get_certs_path()      cert = crypto.X509Certificate( -        open(certs_path + '/leaptestscert.pem').read()) +        open(where('leaptestscert.pem')).read())      key = crypto.X509PrivateKey( -        open(certs_path + '/leaptestskey.pem').read()) +        open(where('leaptestskey.pem')).read())      ca = crypto.X509Certificate( -        open(certs_path + '/cacert.pem').read()) +        open(where('cacert.pem')).read())      #crl = crypto.X509CRL(open(certs_path + '/crl.pem').read())      #cred = crypto.X509Credentials(cert, key, [ca], [crl])      cred = X509Credentials(cert, key, [ca]) @@ -253,19 +254,17 @@ class OpenSSLServerContextFactory:          """Create an SSL context.          This is a sample implementation that loads a certificate from a file          called 'server.pem'.""" -        certs_path = get_certs_path()          ctx = SSL.Context(SSL.SSLv23_METHOD) -        ctx.use_certificate_file(certs_path + '/leaptestscert.pem') -        ctx.use_privatekey_file(certs_path + '/leaptestskey.pem') +        #certs_path = get_certs_path() +        #ctx.use_certificate_file(certs_path + '/leaptestscert.pem') +        #ctx.use_privatekey_file(certs_path + '/leaptestskey.pem') +        ctx.use_certificate_file(where('leaptestscert.pem')) +        ctx.use_privatekey_file(where('leaptestskey.pem'))          return ctx -if __name__ == "__main__": - -    from twisted.python import log -    log.startLogging(sys.stdout) - +def serve_fake_provider():      root = Resource()      root.putChild("provider.json", File("./provider.json"))      config = Resource() @@ -293,3 +292,11 @@ if __name__ == "__main__":      reactor.listenSSL(8443, factory, OpenSSLServerContextFactory())      reactor.run() + + +if __name__ == "__main__": + +    from twisted.python import log +    log.startLogging(sys.stdout) + +    serve_fake_provider() diff --git a/src/leap/gui/tests/test_firstrun_login.py b/src/leap/gui/tests/test_firstrun_login.py new file mode 100644 index 00000000..fa800c23 --- /dev/null +++ b/src/leap/gui/tests/test_firstrun_login.py @@ -0,0 +1,212 @@ +import sys +import unittest + +import mock + +from leap.testing import qunittest +#from leap.testing import pyqt + +from PyQt4 import QtGui +#from PyQt4 import QtCore +#import PyQt4.QtCore  # some weirdness with mock module + +from PyQt4.QtTest import QTest +from PyQt4.QtCore import Qt + +from leap.gui import firstrun + +try: +    from collections import OrderedDict +except ImportError: +    # We must be in 2.6 +    from leap.util.dicts import OrderedDict + + +class TestPage(firstrun.login.LogInPage): +    pass + + +class LogInPageLogicTestCase(qunittest.TestCase): + +    # XXX can spy on signal connections +    __name__ = "register user page logic tests" + +    def setUp(self): +        self.app = QtGui.QApplication(sys.argv) +        QtGui.qApp = self.app +        self.page = TestPage(None) +        self.page.wizard = mock.MagicMock() + +    def tearDown(self): +        QtGui.qApp = None +        self.app = None +        self.page = None + +    def test__do_checks(self): +        eq = self.assertEqual + +        self.page.userNameLineEdit.setText('testuser@domain') +        self.page.userPasswordLineEdit.setText('testpassword') + +        # fake register process +        with mock.patch('leap.base.auth.LeapSRPRegister') as mockAuth: +            mockSignup = mock.MagicMock() + +            reqMockup = mock.Mock() +            # XXX should inject bad json to get error +            reqMockup.content = '{"errors": null}' +            mockSignup.register_user.return_value = (True, reqMockup) +            mockAuth.return_value = mockSignup +            checks = [x for x in self.page._do_checks()] + +            eq(len(checks), 4) +            labels = [str(x) for (x, y), z in checks] +            eq(labels, ['head_sentinel', +                        'Resolving domain name', +                        'Validating credentials', +                        'end_sentinel']) +            progress = [y for (x, y), z in checks] +            eq(progress, [0, 20, 60, 100]) + +            # normal run, ie, no exceptions + +            checkfuns = [z for (x, y), z in checks] +            checkusername, resolvedomain, valcreds = checkfuns[:-1] + +            self.assertTrue(checkusername()) +            #self.mocknetchecker.check_name_resolution.assert_called_with( +                #'test_provider1') + +            self.assertTrue(resolvedomain()) +            #self.mockpcertchecker.is_https_working.assert_called_with( +                #"https://test_provider1", verify=True) + +            self.assertTrue(valcreds()) + +        # XXX missing: inject failing exceptions +        # XXX TODO make it break + + +class RegisterUserPageUITestCase(qunittest.TestCase): + +    # XXX can spy on signal connections +    __name__ = "Register User Page UI tests" + +    def setUp(self): +        self.app = QtGui.QApplication(sys.argv) +        QtGui.qApp = self.app + +        self.pagename = "signup" +        pages = OrderedDict(( +            (self.pagename, TestPage), +            ('providersetupvalidation', +             firstrun.regvalidation.RegisterUserValidationPage))) +        self.wizard = firstrun.wizard.FirstRunWizard(None, pages_dict=pages) +        self.page = self.wizard.page(self.wizard.get_page_index(self.pagename)) + +        self.page.do_checks = mock.Mock() + +        # wizard would do this for us +        self.page.initializePage() + +    def tearDown(self): +        QtGui.qApp = None +        self.app = None +        self.wizard = None + +    # XXX refactor out +    def fill_field(self, field, text): +        """ +        fills a field (line edit) that is passed along +        :param field: the qLineEdit +        :param text: the text to be filled +        :type field: QLineEdit widget +        :type text: str +        """ +        keyp = QTest.keyPress +        field.setFocus(True) +        for c in text: +            keyp(field, c) +        self.assertEqual(field.text(), text) + +    def del_field(self, field): +        """ +        deletes entried text in +        field line edit +        :param field: the QLineEdit +        :type field: QLineEdit widget +        """ +        keyp = QTest.keyPress +        for c in range(len(field.text())): +            keyp(field, Qt.Key_Backspace) +        self.assertEqual(field.text(), "") + +    def test_buttons_disabled_until_textentry(self): +        # it's a commit button this time +        nextbutton = self.wizard.button(QtGui.QWizard.CommitButton) + +        self.assertFalse(nextbutton.isEnabled()) + +        f_username = self.page.userNameLineEdit +        f_password = self.page.userPasswordLineEdit + +        self.fill_field(f_username, "testuser") +        self.fill_field(f_password, "testpassword") + +        # commit should be enabled +        # XXX Need a workaround here +        # because the isComplete is not being evaluated... +        # (no event loop running??) +        #import ipdb;ipdb.set_trace() +        #self.assertTrue(nextbutton.isEnabled()) +        self.assertTrue(self.page.isComplete()) + +        self.del_field(f_username) +        self.del_field(f_password) + +        # after rm fields commit button +        # should be disabled again +        #self.assertFalse(nextbutton.isEnabled()) +        self.assertFalse(self.page.isComplete()) + +    def test_validate_page(self): +        self.assertFalse(self.page.validatePage()) +        # XXX TODO MOAR CASES... +        # add errors, False +        # change done, False +        # not done, do_checks called +        # click confirm, True +        # done and do_confirm, True + +    def test_next_id(self): +        self.assertEqual(self.page.nextId(), 1) + +    def test_paint_event(self): +        self.page.populateErrors = mock.Mock() +        self.page.paintEvent(None) +        self.page.populateErrors.assert_called_with() + +    def test_validation_ready(self): +        f_username = self.page.userNameLineEdit +        f_password = self.page.userPasswordLineEdit + +        self.fill_field(f_username, "testuser") +        self.fill_field(f_password, "testpassword") + +        self.page.done = True +        self.page.on_checks_validation_ready() +        self.assertFalse(f_username.isEnabled()) +        self.assertFalse(f_password.isEnabled()) + +        self.assertEqual(self.page.validationMsg.text(), +                         "Credentials validated.") +        self.assertEqual(self.page.do_confirm_next, True) + +    def test_regex(self): +        # XXX enter invalid username with key presses +        # check text is not updated +        pass + + +if __name__ == "__main__": +    unittest.main() diff --git a/src/leap/gui/tests/test_firstrun_register.py b/src/leap/gui/tests/test_firstrun_register.py index be38e87c..3447fe9d 100644 --- a/src/leap/gui/tests/test_firstrun_register.py +++ b/src/leap/gui/tests/test_firstrun_register.py @@ -220,5 +220,25 @@ class RegisterUserPageUITestCase(qunittest.TestCase):          self.page.paintEvent(None)          self.page.populateErrors.assert_called_with() +    def test_validation_ready(self): +        f_username = self.page.userNameLineEdit +        f_password = self.page.userPasswordLineEdit +        f_passwor2 = self.page.userPassword2LineEdit + +        self.fill_field(f_username, "testuser") +        self.fill_field(f_password, "testpassword") +        self.fill_field(f_passwor2, "testpassword") + +        self.page.done = True +        self.page.on_checks_validation_ready() +        self.assertFalse(f_username.isEnabled()) +        self.assertFalse(f_password.isEnabled()) +        self.assertFalse(f_passwor2.isEnabled()) + +        self.assertEqual(self.page.validationMsg.text(), +                         "Registration succeeded!") +        self.assertEqual(self.page.do_confirm_next, True) + +  if __name__ == "__main__":      unittest.main() | 
