On 02/19/2015 03:51 PM, Petr Vobornik wrote:
https://fedorahosted.org/freeipa/ticket/4307

For ipa-4-1 apply:
- patch 800 (different thread)
- patches 801-806

For master apply:
- patch 800 (different thread)
- patch 807 (different thread)
- patch 801-master
- patches 802-806

Patch 801 allows to use ipalib rpc client in Web UI test suite.
Patches 802-805 are various ui_driver fixes to allow stuff in patch 806.

== [PATCH] 806 webui-ci: otptoken tests ==

Basic otptoken Web UI CI coverage.

tests:
* crud for otptokens as admin
* crud for normal users
* checks fields of adder dialog for both token types and user role
(admin/user)
* token actions as admin (enable, disable, delete)
* token actions as normal user (delete)
* login as normal user with hotp and totp token
* sync token hotp and totp token as normal user and then login

https://fedorahosted.org/freeipa/ticket/4307

== [PATCH] 805 webui-ci: allow custom names for disable/enable actions ==

Not all disable and enable actions are called 'disable' and 'enable'.

== [PATCH] 804 webui-ci: allow to update pkey in post-add in basic-crud
  tests ==

== [PATCH] 803 webui-ci: add post_add_action ==

post add action allows to fill autogenerated values, e.g. a pkey of new
otptoken.

This value can be then used in other subsequent test which would depend
on it - like crud tests.

== [PATCH] 802 webui-ci: fix negative visibility check ==

Allow to define, that element doesn't have to be present on a page for
negative visible checks.

E.g. if element is added only if it's displayed and is removed otherwise.

== [PATCH] 801 webui-ci: support direct IPA API calls ==

Add IPA API support to ui_driver. It leverages new ipalib RPC client's
forms based authentication. It then allows to call an IPA API while
the machine is not an IPA client nor is kerberized.

api's environment values are taken from test configuration and
therefore duplication in ~/.ipa/default.conf is not required.

Since the machine doesn't have to be IPA client, it then also doesn't
have nss database with IPA's CA certificate. Therefore on each API
initialization a new NSS database is created with a CA certificate
downloaded from IPA. This db is deleted in tearDown phase.

Usage:

1. as admin one can immediately call rpc commands, api will be
initialized upon first request and is available under self.api
(assuming self is ui_driver):
   self.api.Command.user_del(USER_ID, **{'continue': True})

2. to reconnect as other user:
   self.reconnect_api(USER_ID, USER_PW)

3. reconnect back as admin:
   self.reconnect_api()


Patch #803 needed rebase.
--
Petr Vobornik
From 643564ca13fac71f303bba9147914aa42a17153b Mon Sep 17 00:00:00 2001
From: Petr Vobornik <pvobo...@redhat.com>
Date: Wed, 7 Jan 2015 13:56:07 +0100
Subject: [PATCH] webui-ci: otptoken tests

Basic otptoken Web UI CI coverage.

tests:
* crud for otptokens as admin
* crud for normal users
* checks fields of adder dialog for both token types and user role (admin/user)
* token actions as admin (enable, disable, delete)
* token actions as normal user (delete)
* login as normal user with hotp and totp token
* sync token hotp and totp token as normal user and then login

https://fedorahosted.org/freeipa/ticket/4307
---
 ipatests/test_webui/test_otptoken.py | 374 +++++++++++++++++++++++++++++++++++
 ipatests/test_webui/ui_driver.py     |   2 +
 2 files changed, 376 insertions(+)
 create mode 100644 ipatests/test_webui/test_otptoken.py

diff --git a/ipatests/test_webui/test_otptoken.py b/ipatests/test_webui/test_otptoken.py
new file mode 100644
index 0000000000000000000000000000000000000000..465dc8da0e3a5de224cf9266829aadb7968eaab5
--- /dev/null
+++ b/ipatests/test_webui/test_otptoken.py
@@ -0,0 +1,374 @@
+# Authors:
+#   Petr Vobornik <pvobo...@redhat.com>
+#
+# Copyright (C) 2014  Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY 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 <http://www.gnu.org/licenses/>.
+
+"""
+OTP Token Web UI Tests
+"""
+
+import base64
+import hashlib
+import hmac
+import struct
+import re
+import time
+from urlparse import urlparse
+
+from ipatests.test_webui.ui_driver import UI_driver
+from ipatests.test_webui.ui_driver import screenshot
+import ipatests.test_webui.data_user as user
+
+ENTITY = 'otptoken'
+TOKEN_RE = r'otpauth://(hotp|totp)/.*:(?P<tokenid>.*)\?'
+
+USER_ID = u'tuser1'
+USER_PW = u'Secret123'
+USER_ADD_DATA = {
+    'givenname': u'test',
+    'sn': u'user1',
+    'userpassword': USER_PW
+}
+
+
+class Token(dict):
+    '''
+    Simplified Copy & Pasted class from OTP API CI tests. Works as Soft Token.
+
+    Initialized from otptoken_add command result
+    '''
+
+    @property
+    def type(self):
+        return self[u'type'].upper()
+
+    @property
+    def tokenid(self):
+        return self[u'ipatokenuniqueid']
+
+    def otp(self, at=0):
+        # I first attempted implementing this with pyotp. However, pyotp has
+        # a critical bug which appeared in testing. I fixed this bug and
+        # submitted it upstream: https://github.com/nathforge/pyotp/pull/9
+        #
+        # However, upstream pyotp appears to be dead. For now, I have
+        # implemented the algorithm myself. In the future, it would be nice
+        # to use python-cryptography here.
+
+        # If the token is time-based, calculate the counter from the time.
+        if self.type == u"TOTP":
+            intrvl = self[u'ipatokentotptimestep']
+            offset = self.get(u'ipatokentotpclockoffset', 0)
+            at = (time.time() + offset + intrvl * at) / intrvl
+
+        # Otherwise, just account for the specified counter offset.
+        elif self.type == u"HOTP":
+            if at < 0:  # Skip invalid test offsets.
+                raise Exception('Invalid HOTP counter offset (at)')
+            at += self.get(u'ipatokenhotpcounter', 0)
+
+        # Create the HMAC of the current counter
+        countr = struct.pack("!Q", at)
+        hasher = getattr(hashlib, self[u'ipatokenotpalgorithm'])
+        digest = hmac.HMAC(self[u'ipatokenotpkey'], countr, hasher).digest()
+
+        # Get the number of digits
+        digits = self[u'ipatokenotpdigits']
+
+        # Truncate the digest
+        offset = ord(digest[-1]) & 0xf
+        binary = (ord(digest[offset + 0]) & 0x7f) << 0x18
+        binary |= (ord(digest[offset + 1]) & 0xff) << 0x10
+        binary |= (ord(digest[offset + 2]) & 0xff) << 0x08
+        binary |= (ord(digest[offset + 3]) & 0xff) << 0x00
+        binary = binary % (10 ** digits)
+
+        return str(binary).rjust(digits, '0')
+
+    def __init__(self, token_result):
+
+        for key, val in token_result.iteritems():
+            if key == 'uri':
+                secret = urlparse(val).query.split(u'&')[1].split(u'=')[1]
+                secret = base64.b32decode(secret)
+                self['ipatokenotpkey'] = secret
+            elif key in ('ipatokenotpdigits', 'ipatokentotptimestep',
+                         'ipatokentotpclockoffset'):
+                self[key] = int(val[0])
+            elif isinstance(val, (list, tuple)):
+                self[key] = val[0]
+            else:
+                self[key] = val
+
+
+class test_otptoken(UI_driver):
+
+    def check_visible_fields(self, user=True, totp=True):
+        '''
+        Check if admin interface contains all fields and self-service only
+        type and description.
+        '''
+        admin = not user
+        self.assert_visible("[name='description']")
+        self.assert_visible("[name='ipatokenuniqueid']", negative=user, present=admin)
+        self.assert_visible("[name='ipatokenowner']", negative=user, present=admin)
+        self.assert_visible("[name='ipatokennotbefore']", negative=user, present=admin)
+        self.assert_visible("[name='ipatokennotafter']", negative=user, present=admin)
+        self.assert_visible("[name='ipatokenvendor']", negative=user, present=admin)
+        self.assert_visible("[name='ipatokenmodel']", negative=user, present=admin)
+        self.assert_visible("[name='ipatokenserial']", negative=user, present=admin)
+        self.assert_visible("[name='ipatokenotpkey']", negative=user, present=admin)
+        self.assert_visible("[name='ipatokenotpalgorithm']", negative=user, present=admin)
+        self.assert_visible("[name='ipatokenotpdigits']", negative=user, present=admin)
+        totp = totp and admin  # visible only in admin interface
+        self.assert_visible("[name='ipatokentotptimestep']", negative=(not totp), present=(totp))
+
+    def token_post_add(self, data=None):
+        '''
+        Check functionality of QR dialog and retrieve configuration url which
+        also contains a token name.
+        '''
+        qr_image_cont = "a[name='qr'] div[name='qr']"
+        uri_cont = "div[name='uri-control']"
+
+        self.assert_visible(qr_image_cont)
+        self.assert_visible(uri_cont, negative=True)
+        self.click_on_link('Show configuration uri')
+        self.assert_visible(uri_cont)
+        self.assert_visible(qr_image_cont, negative=True)
+        config_uri = self.get_text(uri_cont)
+        self.click_on_link('Show QR code')
+        self.assert_visible(qr_image_cont)
+        self.assert_visible(uri_cont, negative=True)
+        self.dialog_button_click('ok')
+
+        match = re.match(TOKEN_RE, config_uri)
+        assert match, "Unable to fetch token ID"
+        tokenid = match.group('tokenid')
+        if data:
+            data['pkey'] = tokenid
+
+    def create_user(self, userid=USER_ID, pw=USER_PW, adddata=USER_ADD_DATA,
+                    logout=True):
+        '''
+        Create user and reset his password.
+        '''
+        # add user
+        self.delete_user(userid)
+        self.api.Command.user_add(userid, **adddata)
+        # reset psw
+        self.init_app(userid, pw)
+        if logout:
+            self.logout()
+
+    def delete_user(self, userid=USER_ID):
+        '''
+        Delete test user
+        '''
+        self.api.Command.user_del(USER_ID, **{'continue': True})
+
+    def create_tokens(self, userid=USER_ID, pw=USER_PW, totp=True, hotp=True):
+        '''
+        Create TOTP and HOTP token for user
+        '''
+        # add tokens
+        self.reconnect_api(USER_ID, USER_PW)
+        totpt = hotpt = None
+        if totp:
+            res = self.api.Command.otptoken_add(**{'all': True})['result']
+            totpt = Token(res)
+        if hotp:
+            res = self.api.Command.otptoken_add(
+                None, **{'type': u'hotp', 'all': True})['result']
+            hotpt = Token(res)
+        return (totpt, hotpt)
+
+    def delete_token(self, token):
+        self.api.Command.otptoken_del(token.tokenid, **{'continue': True})
+
+    @screenshot
+    def test_crud_admin(self):
+        """
+        Basic CRUD: OTPToken - admin
+        """
+        self.init_app()
+        pkey = 'testkey'
+        self.basic_crud(
+            ENTITY,
+            {
+                'pkey': pkey,
+                'add': [
+                    ('callback', lambda args: self.check_visible_fields(False, True), None),
+                    ('textbox', 'ipatokenuniqueid', pkey),
+                    ('textbox', 'description', 'testtoken1'),
+                    ('radio', 'type', 'hotp'),
+                    ('callback', lambda args: self.check_visible_fields(False, False), None),
+                ],
+                'mod': [
+                    ('textbox', 'ipatokenvendor', 'ipa tests'),
+                ],
+            },
+            post_add_action=lambda: self.token_post_add())
+
+    @screenshot
+    def test_actions_admin(self):
+        """
+        Test 'enable', 'disable', 'delete' actions
+        """
+        token = self.api.Command.otptoken_add()['result']
+        self.init_app()
+        tokenid = token['ipatokenuniqueid'][0]
+        self.navigate_to_record(tokenid, entity=ENTITY)
+        self.disable_action('otp_disable')
+        self.enable_action('otp_enable')
+        self.delete_action(ENTITY, tokenid)
+
+    @screenshot
+    def test_crud_selfservice(self):
+        """
+        Basic CRUD: OTPToken - self-service
+        """
+        self.create_user(logout=False)
+
+        data = {
+            'pkey': 'unknown',
+            'add': [
+                ('callback', lambda args: self.check_visible_fields(True, True), None),
+                ('radio', 'type', 'hotp'),
+                ('textbox', 'description', 'testtoken2'),
+                ('callback', lambda args: self.check_visible_fields(True, False), None),
+            ],
+            'mod': [
+                ('textarea', 'description', 'foo'),
+            ],
+        }
+
+        self.basic_crud(
+            ENTITY, data,
+            post_add_action=lambda: self.token_post_add(data)
+        )
+
+        # cleanup
+        self.reconnect_api()
+        self.delete_user(USER_ID)
+
+    @screenshot
+    def test_actions_selfservice(self):
+        """
+        Test 'delete' action - self-service
+        """
+        self.create_user(logout=False)
+        totp, hotp = self.create_tokens(hotp=False)
+        self.navigate_to_record(totp.tokenid, entity=ENTITY)
+        self.delete_action(ENTITY, totp.tokenid)
+
+        # cleanup
+        self.reconnect_api()
+        self.delete_user(USER_ID)
+
+    def login_with_otp(self, token, at, success=True, logout=True):
+        password = USER_PW + token.otp(at)
+        self.login(USER_ID, password)
+        loggedin = self.logged_in()
+        if success:
+            assert loggedin, '%s: user should be logged-in' % token.type
+        else:
+            assert not loggedin, '%s: user should not be logged-in' % token.type
+        if logout:
+            self.logout()
+
+    @screenshot
+    def test_login_otp(self):
+        """
+        Login with TOTP and HOTP tokens
+        """
+
+        self.create_user()
+        # add tokens
+        totp, hotp = self.create_tokens()
+
+        # set auth to tokens
+        self.reconnect_api()
+        self.api.Command.user_mod(USER_ID, **{'ipauserauthtype': u'otp'})
+
+        self.load()
+
+        # fail just with password
+        self.login(USER_ID, USER_PW)
+        assert not self.logged_in(), 'Plain password: user incorrectly logged-in'
+
+        # test otp login
+        self.login_with_otp(totp, 0)
+        self.login_with_otp(hotp, 0)
+        hasher = hashlib.sha1
+
+        self.wait(4)
+
+        # cleanup
+        self.reconnect_api()
+        self.delete_user(USER_ID)
+
+    def synchronize_token(self, token, at, success=True):
+        """
+        From login pages navigates to synchronize token page, fills the form,
+        executes the action and checks result. Then tries to login.
+        """
+
+        if self.get_facet_info()["name"] == 'login':
+            self._button_click("button[title='Sync OTP Token']", self.get_form())
+
+        self.fill_input('user', USER_ID)
+        self.fill_password('password', USER_PW)
+        self.fill_password('first_code', token.otp(at))
+        self.fill_password('second_code', token.otp(at + 1))
+        self._button_click("button[title='Sync OTP Token']", self.get_form())
+        self.wait_for_request()
+        s = 'div.alert-danger[data-name=sync]'
+        self.assert_visible(s, present=False, negative=success)
+        if success:
+            self.assert_facet(None, 'login')
+            self.login_with_otp(token, at + 2)
+        else:
+            self.assert_facet(None, 'sync-otp')
+
+    @screenshot
+    def test_sync_token(self):
+        """
+        Sync TOTP and HOTP tokens using web ui form.
+        """
+
+        self.create_user()
+        totp, hotp = self.create_tokens()
+
+        # set auth to tokens
+        self.reconnect_api()
+        self.api.Command.user_mod(USER_ID, **{'ipauserauthtype': u'otp'})
+
+        self.load()
+        self.synchronize_token(totp, 20)
+        self.synchronize_token(hotp, 20)
+
+        # check distant future, expect fail, assumes that otp config is not
+        # changed
+        self.synchronize_token(totp, 1000000, success=False)
+        self.synchronize_token(hotp, 2000, success=False)
+
+        # cleanup
+        self.reconnect_api()
+        self.delete_token(totp)
+        self.delete_token(hotp)
+        self.delete_user(USER_ID)
diff --git a/ipatests/test_webui/ui_driver.py b/ipatests/test_webui/ui_driver.py
index 86d278b8cbc9232633e31dfcc3af2dff7a38e1cb..1deca37e9efd01caeb9a4572102ce0d46ad82904 100644
--- a/ipatests/test_webui/ui_driver.py
+++ b/ipatests/test_webui/ui_driver.py
@@ -463,6 +463,8 @@ class UI_driver(object):
             auth = self.get_login_screen()
             login_tb = self.find("//input[@type='text'][@name='username']", 'xpath', auth, strict=True)
             psw_tb = self.find("//input[@type='password'][@name='password']", 'xpath', auth, strict=True)
+            login_tb.clear()
+            psw_tb.clear()
             login_tb.send_keys(login)
             psw_tb.send_keys(password)
             psw_tb.send_keys(Keys.RETURN)
-- 
2.1.0

From 504d86ad7e23f68d70eece8dacf79066b1a4ef4a Mon Sep 17 00:00:00 2001
From: Petr Vobornik <pvobo...@redhat.com>
Date: Fri, 9 Jan 2015 16:17:27 +0100
Subject: [PATCH] webui-ci: allow custom names for disable/enable actions

Not all disable and enable actions are called 'disable' and 'enable'.
---
 ipatests/test_webui/ui_driver.py | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/ipatests/test_webui/ui_driver.py b/ipatests/test_webui/ui_driver.py
index e62b0b418e8932b921eb509f3944b2274ff0edd5..86d278b8cbc9232633e31dfcc3af2dff7a38e1cb 100644
--- a/ipatests/test_webui/ui_driver.py
+++ b/ipatests/test_webui/ui_driver.py
@@ -1516,22 +1516,22 @@ class UI_driver(object):
         link.click()
         self.wait()
 
-    def enable_action(self):
+    def enable_action(self, action_name='enable'):
         """
         Execute and test 'enable' action panel action.
         """
         title = self.find('.active-facet div.facet-title', By.CSS_SELECTOR, strict=True)
-        self.action_list_action('enable')
+        self.action_list_action(action_name)
         self.wait_for_request(n=2)
         self.assert_no_error_dialog()
         self.assert_class(title, 'disabled', negative=True)
 
-    def disable_action(self):
+    def disable_action(self, action_name='disable'):
         """
         Execute and test 'disable' action panel action.
         """
         title = self.find('.active-facet div.facet-title', By.CSS_SELECTOR, strict=True)
-        self.action_list_action('disable')
+        self.action_list_action(action_name)
         self.wait_for_request(n=2)
         self.assert_no_error_dialog()
         self.assert_class(title, 'disabled')
-- 
2.1.0

From b6db447af66fca626b84a5c546bf729b0d5ebd85 Mon Sep 17 00:00:00 2001
From: Petr Vobornik <pvobo...@redhat.com>
Date: Fri, 9 Jan 2015 16:16:57 +0100
Subject: [PATCH] webui-ci: allow to update pkey in post-add in basic-crud
 tests

---
 ipatests/test_webui/ui_driver.py | 6 ++----
 1 file changed, 2 insertions(+), 4 deletions(-)

diff --git a/ipatests/test_webui/ui_driver.py b/ipatests/test_webui/ui_driver.py
index 2ff5faa578189330cb305cab9f7e1a4c885d5ce5..e62b0b418e8932b921eb509f3944b2274ff0edd5 100644
--- a/ipatests/test_webui/ui_driver.py
+++ b/ipatests/test_webui/ui_driver.py
@@ -1382,8 +1382,6 @@ class UI_driver(object):
         if not parent_entity:
             parent_entity = entity
 
-        pkey = data['pkey']
-
         # 1. Open Search Facet
         if navigate:
             self.navigate_to_entity(parent_entity)
@@ -1401,7 +1399,7 @@ class UI_driver(object):
         self.find_record(parent_entity, data, search_facet)
 
         # 3. Navigate to details facet
-        self.navigate_to_record(pkey)
+        self.navigate_to_record(data['pkey'])
         self.assert_facet(entity, default_facet)
         self.wait_for_request(0.5)
         if default_facet != details_facet:
@@ -1422,7 +1420,7 @@ class UI_driver(object):
 
         # 5. Delete record
         if delete:
-            self.delete_record(pkey, data.get('del'))
+            self.delete_record(data['pkey'], data.get('del'))
 
     def add_table_record(self, name, data, parent=None):
         """
-- 
2.1.0

From 8a91c69d302d3c6decd220a8124e1d274ff7c619 Mon Sep 17 00:00:00 2001
From: Petr Vobornik <pvobo...@redhat.com>
Date: Wed, 18 Feb 2015 17:42:02 +0100
Subject: [PATCH] webui-ci: add post_add_action

post add action allows to fill autogenerated values, e.g. a pkey of new
otptoken.

This value can be then used in other subsequent test which would depend
on it - like crud tests.
---
 ipatests/test_webui/ui_driver.py | 8 ++++++--
 1 file changed, 6 insertions(+), 2 deletions(-)

diff --git a/ipatests/test_webui/ui_driver.py b/ipatests/test_webui/ui_driver.py
index 0b48cda6fd26bc3bfe8b8acb982d4ca19e5a8dbe..2ff5faa578189330cb305cab9f7e1a4c885d5ce5 100644
--- a/ipatests/test_webui/ui_driver.py
+++ b/ipatests/test_webui/ui_driver.py
@@ -1260,7 +1260,7 @@ class UI_driver(object):
 
     def add_record(self, entity, data, facet='search', facet_btn='add',
                    dialog_btn='add', delete=False, pre_delete=True,
-                   dialog_name='add', navigate=True):
+                   dialog_name='add', navigate=True, post_add_action=None):
         """
         Add records.
 
@@ -1309,6 +1309,9 @@ class UI_driver(object):
             self.dialog_button_click('ok')
             self.wait_for_request()
 
+        if post_add_action:
+            post_add_action()
+
         # check for error
         self.assert_no_error_dialog()
         self.wait_for_request()
@@ -1353,6 +1356,7 @@ class UI_driver(object):
                    add_facet_btn='add',
                    add_dialog_btn='add',
                    add_dialog_name='add',
+                   post_add_action=None,
                    update_btn='save',
                    breadcrumb=None,
                    navigate=True,
@@ -1389,7 +1393,7 @@ class UI_driver(object):
         # 2. Add record
         self.add_record(parent_entity, data, facet=search_facet, navigate=False,
                         facet_btn=add_facet_btn, dialog_name=add_dialog_name,
-                        dialog_btn=add_dialog_btn
+                        dialog_btn=add_dialog_btn, post_add_action=post_add_action
                         )
 
         # Find
-- 
2.1.0

From 9599d9e7c4dbb204dede3418e9e33c3fa36e00e1 Mon Sep 17 00:00:00 2001
From: Petr Vobornik <pvobo...@redhat.com>
Date: Wed, 18 Feb 2015 17:38:45 +0100
Subject: [PATCH] webui-ci: fix negative visibility check

Allow to define, that element doesn't have to be present on a page for
negative visible checks.

E.g. if element is added only if it's displayed and is removed otherwise.
---
 ipatests/test_webui/ui_driver.py | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)

diff --git a/ipatests/test_webui/ui_driver.py b/ipatests/test_webui/ui_driver.py
index 1ed7d7e0ad249f2d874985b471f4b250677baae7..0b48cda6fd26bc3bfe8b8acb982d4ca19e5a8dbe 100644
--- a/ipatests/test_webui/ui_driver.py
+++ b/ipatests/test_webui/ui_driver.py
@@ -1709,14 +1709,15 @@ class UI_driver(object):
         else:
             assert not state, "Undo button visible. Field: %s" % field
 
-    def assert_visible(self, selector, parent=None, negative=False):
+    def assert_visible(self, selector, parent=None, negative=False, present=True):
         """
         Assert that element defined by selector is visible
         """
         if not parent:
             parent = self.get_form()
-        el = self.find(selector, By.CSS_SELECTOR, parent, strict=True)
-        visible = el.is_displayed()
+        el = self.find(selector, By.CSS_SELECTOR, parent, strict=present)
+        on_page = el is not None
+        visible = on_page and el.is_displayed()
         if negative:
             assert not visible, "Element visible: %s" % selector
         else:
-- 
2.1.0

From 4ca6758d48e04e3fb23f8a095bdc17e6ae40258e Mon Sep 17 00:00:00 2001
From: Petr Vobornik <pvobo...@redhat.com>
Date: Fri, 12 Dec 2014 16:18:34 +0100
Subject: [PATCH] webui-ci: support direct IPA API calls

Add IPA API support to ui_driver. It leverages new ipalib RPC client's
forms based authentication. It then allows to call an IPA API while
the machine is not an IPA client nor is kerberized.

api's environment values are taken from test configuration and
therefore duplication in ~/.ipa/default.conf is not required.

Since the machine doesn't have to be IPA client, it then also doesn't
have nss database with IPA's CA certificate. Therefore on each API
initialization a new NSS database is created with a CA certificate
downloaded from IPA. This db is deleted in tearDown phase.

Usage:

1. as admin one can immediately call rpc commands, api will be
initialized upon first request and is available under self.api
(assuming self is ui_driver):
  self.api.Command.user_del(USER_ID, **{'continue': True})

2. to reconnect as other user:
  self.reconnect_api(USER_ID, USER_PW)

3. reconnect back as admin:
  self.reconnect_api()
---
 ipatests/test_webui/ui_driver.py | 112 ++++++++++++++++++++++++++++++++++++++-
 1 file changed, 111 insertions(+), 1 deletion(-)

diff --git a/ipatests/test_webui/ui_driver.py b/ipatests/test_webui/ui_driver.py
index 4b382d8fb2cb1f56bda05b6ae624a41473981340..1ed7d7e0ad249f2d874985b471f4b250677baae7 100644
--- a/ipatests/test_webui/ui_driver.py
+++ b/ipatests/test_webui/ui_driver.py
@@ -52,8 +52,11 @@ try:
     NO_YAML = False
 except ImportError:
     NO_YAML = True
-from urllib2 import URLError
+from urllib2 import URLError, urlparse
 from ipaplatform.paths import paths
+from ipapython import certdb, ipautil
+from ipapython.ipautil import run
+from ipalib import x509, api
 
 ENV_MAP = {
     'MASTER': 'ipa_server',
@@ -117,10 +120,31 @@ class UI_driver(object):
         if NO_SELENIUM:
             raise nose.SkipTest('Selenium not installed')
 
+    @property
+    def api(self):
+        """
+        ipalib API
+        """
+        if not self._api:
+            self.init_api()
+        return self._api
+
+    @property
+    def nss_db(self):
+        """
+        NSS DB for IPA lib rpc client
+        """
+        if not self._nss_db:
+            self._nss_db = self._prepare_nss_dir(self.config['ipa_server'])
+        return self._nss_db
+
     def setup(self, driver=None, config=None):
         self.request_timeout = 30
         self.driver = driver
         self.config = config
+        self._env_backup = None
+        self._api = None
+        self._nss_db = None
         if not config:
             self.load_config()
         if not self.driver:
@@ -129,6 +153,9 @@ class UI_driver(object):
 
     def teardown(self):
         self.driver.quit()
+        if self._nss_db:
+            self._nss_db.close()
+        self._restore_env()
 
     def load_config(self):
         """
@@ -167,6 +194,89 @@ class UI_driver(object):
         if 'type' not in c:
             c['type'] = DEFAULT_TYPE
 
+    def _prepare_nss_dir(self, ipa_server):
+        """
+        Create temporary NSS Database with IPA server CA cert. CA cert is
+        downloaded over HTTP from IPA server.
+        """
+
+        # create new NSSDatabase
+        tmp_db = certdb.NSSDatabase()
+        pwd_file = ipautil.write_tmp_file(ipautil.ipa_generate_password())
+        tmp_db.create_db(pwd_file.name)
+
+        # download and add cert
+        url = urlparse.urlunparse(('http', ipautil.format_netloc(ipa_server),
+                                   '/ipa/config/ca.crt', '', '', ''))
+        stdout, stderr, rc = run([paths.BIN_WGET, "-O", "-", url])
+        certs = x509.load_certificate_list(stdout, tmp_db.secdir)
+        ca_certs = [cert.der_data for cert in certs]
+        for i, cert in enumerate(ca_certs):
+            tmp_db.add_cert(cert, 'CA certificate %d' % (i + 1), 'C,,')
+
+        return tmp_db
+
+    def init_api(self):
+        """
+        Initialize ipalib API so we can use API commands in UI tests directly.
+        Uses forms-based authentication. It allows Web UI tests to be run a
+        system which is not an IPA client.
+        """
+
+        self._api = api
+        self._env_backup = {}
+
+        def set_env(key, val):
+            self._env_backup[key] = api.env[key]
+            object.__setattr__(api.env, key, val)
+            api.env._Env__d[key] = val
+
+        ipa_server = self.config['ipa_server']
+        set_env('xmlrpc_uri', "https://"; + ipa_server + "/ipa/xml")
+        set_env('jsonrpc_uri', "https://"; + ipa_server + "/ipa/json")
+        set_env('realm', self.config["ipa_realm"])
+        set_env('domain', self.config["ipa_domain"])
+        set_env('basedn', ipautil.realm_to_suffix(self.config["ipa_realm"]))
+
+        self.reconnect_api()
+
+    def _restore_env(self):
+        """
+        Revert changes in API env
+        """
+        if not self._env_backup or self._api:
+            return
+
+        env = self.api.env
+
+        def restore(key, val):
+            object.__setattr__(env, key, val)
+            env._Env__d[key] = val
+
+        for k, v in self._env_backup:
+            restore(k, v)
+
+        self._env_backup = None
+
+    def reconnect_api(self, user=None, password=None):
+        """
+        Reconnect rpcclient as different user with password
+        """
+        if not user:
+            user = self.config['ipa_admin']
+        if not password:
+            password = self.config['ipa_password']
+        self.disconnect_api()
+        self.api.Backend.rpcclient.connect(
+            nss_dir=self.nss_db.secdir,
+            user=user,
+            password=password
+        )
+
+    def disconnect_api(self):
+        if self._api and self.api.Backend.rpcclient.isconnected():
+            self.api.Backend.rpcclient.disconnect()
+
     def get_driver(self):
         """
         Get WebDriver according to configuration
-- 
2.1.0

-- 
Manage your subscription for the Freeipa-devel mailing list:
https://www.redhat.com/mailman/listinfo/freeipa-devel
Contribute to FreeIPA: http://www.freeipa.org/page/Contribute/Code

Reply via email to