On 05/11/2015 01:25 PM, Milan Kubik wrote:
On 05/07/2015 01:38 PM, Petr Vobornik wrote:
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.


Hi, thanks for the patches.

Please, fix pep8 complaints in 803, 805 and 806.


$ git diff HEAD~6 -U0 | pep8 --diff

returns 20x E501 line too long

IMO, it's better this way for better code readability.


Also, change the header in 806 to the shorter version, please.

Fixed, patches were regenerated.


#
# Copyright (C) 2015  FreeIPA Contributors see COPYING for license
#

Patches 801, 802 and 804 look good to me.
The test cases in 806 look good to me as well.

Milan

--
Petr Vobornik
From 84ced4fce411836a5b27ac352ea540faceb52c41 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 | 359 +++++++++++++++++++++++++++++++++++
 ipatests/test_webui/ui_driver.py     |   2 +
 2 files changed, 361 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..63e212459c2261d9fc7b069f58652aae94adb56d
--- /dev/null
+++ b/ipatests/test_webui/test_otptoken.py
@@ -0,0 +1,359 @@
+#
+# Copyright (C) 2015  FreeIPA Contributors see COPYING for license
+#
+
+"""
+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 ee44225b4cc42424b035bb46cb2a5d1d5d630b35 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 7d9101e391ff79c75d8818819bd4f46793dc9b81 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 0bba609e6453b544bade011d217c91f17aa3d8fd 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 4349164a019e5fc70e9c702d546020497c03075d 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 14fa9a1a5951e0f8b15ffc2d5bb6b8ed5506ca0b 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