Showing posts with label G-Reg. Show all posts
Showing posts with label G-Reg. Show all posts

Saturday, June 27, 2015

Patch Management System - developed, maintained, enhanced using WSO2 Carbon products

WSo2 support patching in brief

WSO2 support system issues patches to fix defects that are found in product setups. This patching procedure needs to be methodically handled as the fix that you are applying should correctly solve the issue, it should not introduce regression, it should be committed to relevant public code base, it should be committed to support environment and validated that it correctly fit in with existing code base.

Patch Management Tool (PMT)

In an environment where you have large customer base and expects many clarifications daily, there are chances of developers missing certain best practices. Therefore a process that forces and reminds you of what need to be done while the process should be embedded to the system. WSO2 Patch Management Tool, PMT was built for above purpose.

In the above process, we also need to maintain a database of patch information. In WSO2 support system, applying a patch to a server involves a method which ensures that a new fix is correctly applied to the product binaries. This patch applying procedure mainly relies on a numbering system. This means every patch has its own number. Since we support maintenance for many platform versions at the same time, there are parallel numbering systems for each WSO2 Carbon platform version. All above data should be correctly archived and should be available as an index when you need to create a new patch. PMT facilitates this by being the hub for all patches. Any developer who creates a new patch obtains the patch number for his work from PMT.

WSO2 support system involves many developers working parallel in different or same product components, based on the customer issues they are handling. In such space, it is important to have meta data such as the jar versions that are patched, commit revisions, patch location, integration test cases, integration test revisions, developer/QA who worked on the patch, patch released date to be archived. These information are needed at a time of a conflict or an error. PMT facilitates an environment to capture these information.

So as described in above the main purposes of PMT are managing patching process, generating patch numbers, archiving patch metadata and providing search facility. This was initially  introduced and designed by Samisa as a pet project for which Lasindu, Pulasthi helped with writing extensions as part of their intern projects and Yumani with QAing.

To cater the project requirements, WSO2 Governance Registry was chosen as the back end server with WSO2 user base connected via WSO2 IS as the user store and MySQL for registry. Later WSO2 User Engagement Server was integrated as  the presentation layer, using Jaggery framework to develop the presentation logic. From WSO2 G-Reg, we are using the RXT, LifeCycle and handler concepts, search/ filtering facilities and governance API. From WSO2 UES we are using webapp hosting capability. WSO2 IS is LDAP user store.

In the proceeding sections I will briefly describe how PMT evolved with different product versions, absorbing new features and enhancements.

First version in wso2greg-4.5.1

PMT was initially designed on G-Reg 4.5.1 version. Using the RXT concept, an artifact type called patch was built. This is to capture all metadata related to a patch. It also defines a storage path for patch metadata, a listing view which provides a quick glance on the existing patches. Few important parts of the rxt is discussed below;

Our requirement was to capture data on JIRA's basic information, client/s to whom its issued, people involved, dates, related documentation and repositories. So the RXT was categorized into tables as Overview, People Information, JIRA Information, Patch Information, Test Information and Dates.

<content>
<table name="Overview">
</table>
<table name="People Involved">
</table>
<table name="Patch Information">
</table>
<table name="Test Information">
</table>
<table name="Dates">
</table>
</content>

Each above <table> has the attributes related to them. Most of these were captured via <field type="options"> or <field type="text">

Above RXT (patchRXT) was associated to a Lifecycle to manage the patching process. Patch LifeCycle involves main stages such as Development, ReadyForQA, Testing, Release. Each above state includes a set of check list items, which lists the tasks that a developer or QA needs to following while in a particular Lifecycle state.

Sample code segment below shows the configuration of the 'testing' state:

  <state id="Testing">
                    <datamodel>
                        <data name="checkItems">
                            <item name="Verified patch zip file format" forEvent="Promote">
                            </item>                           
                            <item name="Verified README" forEvent="Promote">
                            </item>
                            <item name="Verified EULA" forEvent="Promote">
                            </item>
                            <item name="Reviewed the automated tests provided for the patch">
                            </item>
                            <item name="Reviewed info given on public/support commits" forEvent="Promote">
                            </item>
                            <item name="Verified the existance of previous patches in the test environment" forEvent="Promote">
                            </item>
                            <item name="Auotmated tests framework run on test environment">
                            </item>
                            <item name="Checked md5 checksum for jars for hosted and tested" forEvent="Promote">
                            </item>
                            <item name="Patch was signed" forEvent="Promote">
                            </item>
                            <item name="JIRA was marked resolved" forEvent="Promote">
                            </item>
                        </data>   
                         <data name="transitionUI">
                            <ui forEvent="Promote" href="../patch/Jira_tanstionUI_ajaxprocessor.jsp"/>
                            <ui forEvent="ReleasedNotInPublicSVN" href="../patch/Jira_tanstionUI_ajaxprocessor.jsp"/>
                            <ui forEvent="ReleasedNoTestsProvided" href="../patch/Jira_tanstionUI_ajaxprocessor.jsp"/>
                            <ui forEvent="Demote" href="../patch/Jira_tanstionUI_ajaxprocessor.jsp"/>
                        </data>                                        
                    </datamodel>
                    <transition event="Promote" target="Released"/>
                    <transition event="ReleasedNotInPublicSVN" target="ReleasedNotInPublicSVN"/>
                    <transition event="ReleasedNoTestsProvided" target="ReleasedNoTestsProvided"/>
                    <transition event="Demote" target="FailedQA"/>
                </state>

As you may have noticed the Lifecycle transitionUI in which user is given an additional interface in between state changes. In above, on completion of all check list items of the testing state and at the state transition, it pauses to log the time that was spent on the tasks. This information will directly update the live support JIRA and is used for billing purposes. The ui-handlers were used to generate this transition UI dynamically. Later in the cycle wee removed it when Jagath and Parapran introduced a new time logging web application.

The various paths that the patch might need to be parked given the environment factors, we had to introduce different intermediate states such as 'ReleasedNotInPublicSVN', 'ReleasedNoTestsProvided'.  <transition event> tag was helpful in this. For example a patch can be promoted to 'released', or 'ReleasedNotInPublicSVN' or 'ReleasedNoTestsProvided' states or it can be demoted to FailedQA' states using <transition event> option. 

 

Performance Issues in G-Reg 4.5.1

When compared with the low concurrency rate we had at the time, the response time was high while PMT is hosted in G-Reg-4.5.1. This was even observed in JIRA details page. Suspect was that a *social* feature which pops up worklist items in the UI, as per the design at the time, use to run at each page refresh. Also there was a delay when working with Lifecycle state checklist items too.

Migrated to G-Reg 4.5.3

Considering the performance problems and some of the new enhancements to RXTs, PMT was migrated to G-Reg 4.5.3. Migration process was very smooth; there were no database schema changes. We were able to point to the same database that was used earlier. Above issue was solved in new version of G-Reg.

There was also RXT related enhancements, where we could now have a pop-up calender for dates opposed to the text box in the previous version.Filtering; In the earlier version patch filtering was done through a custom development which we had deployed in CARBON_HOME/repository/component/lib. But in the new version this was an inbuilt feature. There were also enhancements in G-Reg's governance API that we could laverage in generating reports and patch numbers. 

Moving historical data to live environment.

Prior to PMT, there was a use of Google spreadsheets to maintain patches related metadata. Since PMT is being used as repository to 'search' patches and related information; it was time to move old data also into PMT. All the previous data that were captured in 6 different spread sheets were moved to PMT. This involved data analysis, since some of the field names were different from spreadsheet to spreadsheet and from spreadsheets to PMT. After background adjustments, Ashan from support team moved the historical data by retrieving data from spreadsheets using dataservices and writing them to PMT using a RemoteRegistry API  [2 -svn location of gov-api code].

This added 900+ patches to the live system and also effected performance as below. This was done using data service to get data from spreadsheets and a java client to add them to PMT [attached].


Looking for new avenues

Scalability:

With the patch database becoming larger by data migrated from archives, as well as PMT being used daily by rotational support teams; there arose an issue where you get an unpredictable stability issues. PoolExhaustedException was one of which that was reproducible when the loaded database is idle for couple of days[1].

After checking into details, leading G-Reg engineers Ajith, Shelan proposed some changes to data-sources configuration which helped.

Introduced 'removeAbandoned' and 'removeAbandonedTimeout':
<removeAbandoned="true"
removeAbandonedTimeout="<This value should be more than the longest possible running transaction>"
logAbandoned="true">

Updated 'maxActive' to a higher value than the default value of 80.
<maxActive>80</maxActive>

But we had couple of more problems, we noticed that it the response time for listing the patches is unreasanably high. Some of the reasons that were isolated with the help of Ajith was that the default setting for versioning which was set to true. In G-reg Lifecycle related data used to be stored storing as properties, so when <versioningProperties>true</versioningProperties> is set, it massively grows the REG_RESOURCE_PROPERTY and REG_PROPERTY tables with the resource updates.The patch LifeCycle (contains 20 check list items) , when one patch resource going through all the LC states, it is adding more than 1100 records to REG_RESOURCE_PROPERTY and REG_PROPERTY when versioning is set to true.

Summary of Ajith's tests;

Versioning on:
Adding LC : 22
One click on check list item : 43
One promote or demote: 71.

Versoning off:
Adding LC : 22
One click on check list item : 00
One promote or demote: 24.

As a solution following were done;
1) Created a script to delete the unwanted properties

2) Disabled the versioning properties, comments and Ratings, from static configurations in registry.xml [2 - https://wso2.org/jira/browse/REGISTRY-1610]
<staticConfiguration>
<versioningProperties>false</versioningProperties>
<versioningComments>false</versioningComments>
<versioningTags>true</versioningTags>
<versioningRatings>false</versioningRatings>
<servicePath>/trunk/services/</servicePath>
</staticConfiguration>

There was also another bug found related to artifact listing. There is a background task in G-Reg to cache all the generic artifacts. That help us to reduce the database calls when we need to retrieve the artifacts(get from cache instead of database).In G-Reg 4.5.3 it doesn't work for the custom artifacts such as patchRXT and it only works for service,policy,wsdl and schema.This issue was patched by the GReg team.

Process Validation:

With increase of number of users daily basis, fastening link with the process became very important. The existing UI at that time was G-Reg management console, but our requirement was to validate Lifecycle events with data in RXT.


Jaggery based web application and GReg upgrade

GReg migration to 4.6.0 version was done by Parapran to overcome above issues which was known and fixed in new GReg and a new jaggery based web application was developed by support intern at the time, Nazmin as per Yumani's design and Jagath and team's valuable feedback.

The intention of the new PMT application was to provide a very user friendly view and to assure the developers of the patches follow all necessary steps in creating a patch. For example a developer cannot proceed from the 'development' state if he has not updated the support JIRA, public JIRA, svn revision fields. If he selects to say automation testing is 'Not possible' he has to give reasons. If he has done tests, he has to give the test commit location etc. Yes, we had occasional disturbed use cases. Special thanks to Paraparan and Ashan who fixed these cases with no time.


Image 1: Patch Listing


Image 2: Patch Details

This application was highly appreciated by the users as it allowed auto generation of patch numbers, pre-populated data for customer projects, users, products and versions etc. As promised it duly validates all mandatory inputs by creating a direct bind to user activities.

Today, the application has been further developed to capture various patch categories such as ported patches, critical patches, preQA patches. GReg based life cycle is also further developed now to cator pre Patch creation and post patch creation tasks. It was Inshaf who implemented these enhancement. We are also working on new features such as updating customers with the ETAs for patches based on three point estimation, looping leads and rigorously following-up in delays.

The application was also deployed in an HA setup by Chamara, Inshaf and Ashan for higher scalability, where the back-end GReg is clustered to 4 nodes fronted by Nginx which is sitting between the client application and the BE. We have about 100+ engineers accessing this application daily with around 15 concurrency for adding patches to patch queue, generating patch numbers, progressing patches through patch life cycle and searching for meta information.

Additionally the PMT database is routinely queried for several other client programs such Patch Health clients, where we generate reports on patches which are not process complete, Service Pack generation where the whole of database is read to extract patches belonging to a given product version and its kernal, Reports for QA at the time of the release testing where they seek for use cases patched by the customer. Most of these client applications are written using governance API.

PMT is a good example of a simple use case which expanded to a full scale system. It is much used in WSO2 support today and we extended it to have many new features to support the increasing support demands. We have been able to leverage Jaggery framework to all UI level enhancements. Governance registry's ability of defining any type of governance asset and customizable life cycle management feature pioneered the inception of this tool and helped in catering the different data patterns that we wanted to preserve and life cycle changes that needed to be added later according to the changes in the process. We were able to add customized UIs to the application with the use of handler concept in GReg. When the front end jaggery web application was introduced, we had a very seamless integration since the back-end could be accessed via governance API. Increasing load demands were well supported by Carbon clustering and high availability concepts.

Tuesday, December 20, 2011

Adding a human readable media type to G-Reg

Adding a human readable media type is a new feature that will be available in the latest WSO2 G-Reg 4.1.1 packs.

The use of this feature comes in when you need to name you media types using something that is more readable than a technical term. For an example say we have media types such as image/x-coreldrawpattern with .pat extension and chemical/x-vamas-iso14976 with .vms extension or
application/vnd.symbian.install with .sis extension.  It is really hard to remember these names.

This difficulty can be eliminated using human readable  media types feature.

Following is how to configure this feature.

  1. Open G-Reg-HOME/repository/conf/mime.types file. This will show you all the supported the mime.types. 
  2. Select a mime type which you need to be mapped to a name of your choice.
  3. Open G-Reg-HOME/repository/conf/mime.mappings file. This is where we are going to add the new name that we are going to use for the given mime type.  The new name you add in this file will map the extension given in the mime.type file.
  4. In mime.mappings file, copy the mime-type wanted to change and tab 5 times and type the custom name. See below for an example: line 20
    #
    # Copyright 2005-2011 WSO2, Inc. (http://wso2.com)
    #
    # Licensed under the Apache License, Version 2.0 (the "License");
    # you may not use this file except in compliance with the License.
    # You may obtain a copy of the License at
    #
    # http://www.apache.org/licenses/LICENSE-2.0
    #
    # Unless required by applicable law or agreed to in writing, software
    # distributed under the License is distributed on an "AS IS" BASIS,
    # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    # See the License for the specific language governing permissions and
    # limitations under the License.
    #
    
    # This file is to define the human readable media type for a mime type. 
    # Eg:- 
    # text/plain                                      txt text
    application/wsdl+xml                                  mywsdl
    
    
  5. Save and close the file.
  6. Restart the server.
Lets follow these steps to witness the adjustment :)
  1. Login to G-Reg admin console
  2. Navigate to Resource > Browse
  3. Select to add a new resource.
  4. Add a new resource of the type that you adjusted above. When you select the resource you will see the media type that you gave appear in the 'media type' field. (see image below)




Friday, November 18, 2011

Connecting WSO2 G-Reg to a secured & confidential external user store

From Carbon 3.2.0 onwards we have an embeded LDAP user store which is accessible via port 10389 in default settings. The server also configurable to use LDAP to connect to an external user store. If we need this user store to be secured and confidential it need to be configured to connect via LDAPS.

In this post I will be demonstrating how to connect WSO2 G-REg 4.0.0 to an external user store which will be accessed via secured transport (where the tracffic is transmitted via secured transport).

As pre-requisites we will need WSO2 G-REG 4.0.0 installation which you can download from here. Also we need Apache Directory Server which, we will be using as the LDAP server. You should also have JDK 1.5 or above installed.

With pre-requisites ready, we simply have to follow the steps given below.

Step 1:
Firstly you need to install WSO2 G-Reg 4.0.0 which can be downloaded from here. Extract the .zip file to a location of your preference.

Step 2:

As I mentioned earlier also WSO2 G-Reg is already configured with an internal LDAP user store. In order to connect to an external user store we need to disconnet the existing connection. To achieve this we need to uncomment settings from usermgmt.xml and embedded-ldap.xml. These files reside in GREG_HOME/repository/conf folder.

Lets do these changes like this.
in usermgt.xml comment the default user store manager configuration which is given like below.

<UserStoreManager class="org.wso2.carbon.user.core.ldap.ApacheDSUserStoreManager">
            <Property name="ReadOnly">false</Property>
            <Property name="ConnectionURL">ldap://localhost:${Ports.EmbeddedLDAP.LDAPServerPort}</Property>
            <Property name="ConnectionName">uid=admin,ou=system</Property>
            <Property name="ConnectionPassword">admin</Property>
            <Property name="passwordHashMethod">SHA</Property>
            <Property name="UserNameListFilter">(objectClass=person)</Property>
            <Property name="UserEntryObjectClass">wso2Person</Property>
            <Property name="UserSearchBase">ou=Users,dc=wso2,dc=org</Property>
            <Property name="UserNameSearchFilter">(&amp;(objectClass=person)(uid=?))</Property>
            <Property name="UserNameAttribute">uid</Property>
            <Property name="PasswordJavaScriptRegEx">[\\S]{5,30}</Property>
            <Property name="UsernameJavaScriptRegEx">[\\S]{3,30}</Property>
            <Property name="UsernameJavaRegEx">^[^~!@#$;%^*+={}\\|\\\\&lt;&gt;]{3,30}$</Property>
            <Property name="RolenameJavaScriptRegEx">[\\S]{3,30}</Property>
            <Property name="RolenameJavaRegEx">^[^~!@#$;%^*+={}\\|\\\\&lt;&gt;]{3,30}$</Property>
            <Property name="ReadLDAPGroups">true</Property>
            <Property name="WriteLDAPGroups">true</Property>
            <Property name="EmptyRolesAllowed">true</Property>
            <Property name="GroupSearchBase">ou=Groups,dc=wso2,dc=org</Property>
            <Property name="GroupNameListFilter">(objectClass=groupOfNames)</Property>
            <Property name="GroupEntryObjectClass">groupOfNames</Property>
            <Property name="GroupNameSearchFilter">(&amp;(objectClass=groupOfNames)(cn=?))</Property>
            <Property name="GroupNameAttribute">cn</Property>
            <Property name="MembershipAttribute">member</Property>
        </UserStoreManager>


In embedded-ldap.xml you need to set "enabled" property in "EmbeddedLDAP" to false.
 <Property name="enable">true</Property>


Step 3:
Now we need to enable configurations for the externanal LDAP server. But lets wait until we configure our user store as we still don't have those details with us.

Step 4:
Now we need to install Apache Directry Studio. You can download the .zip ditribution, unzip and use it. The other choice is to use the binary installer. In my case I used the binary installer. I was taken through an installation wizard which prompted me to set locations for the inatalltion, instances and where to keep the startup, who is the default user etc.


Step 5:
Lets enable SSL in ADS. you need to navigate to ADS_INSTANCE_HOME/default/conf/ and open server.xml. Here search for 'tcpTransport address' and check if SSL is enabled. It is enabled by default. If not you need to update with this.
 


Step 6:
In this setup I am going to use a signed certificate to use in our LDAPS communitcation. I will be using keytool to generate the required keystore and certificates.

Since am going have my LDAP server setup with an SSL server certificate, I must obtain a signed certificate for the server. To achieve this I will be using java keytool and create a self signed certificate (a public/private key pair).

 keytool -genkey -alias carbon_server -keyalg RSA -keystore carbon_server.jks -storepass xxxxxx -validity 730

What is your first and last name?   [Unknown]:  Yumani Ranaweera What is the name of your organizational unit?   [Unknown]:  QA What is the name of your organization?   [Unknown]:  WSO2 What is the name of your City or Locality?   [Unknown]:  Colombo What is the name of your State or Province?   [Unknown]:  Western What is the two-letter country code for this unit?   [Unknown]:  SL Is CN=Yumani Ranaweera, OU=QA, O=WSO2, L=Colombo, ST=Western, C=SL correct?   [no]:  yes Enter key password for <carbon_server>     (RETURN if same as keystore password):  Re-enter new password:


Step 7:
After this we need to update the LDAP server (apacheDS) configuration to use our keystore files. to do this navigate to ADS_INSTANCE_HOME/default/conf/ and open server.xml. Update "keystoreFile" in following segment;

<ldapServer id="ldapServer"
            allowAnonymousAccess="false"
            saslHost="ldap.example.com"
            saslPrincipal="ldap/ldap.example.com@EXAMPLE.COM"
            searchBaseDn="ou=users,ou=system"
            maxTimeLimit="15000"
            maxSizeLimit="1000"
            keystoreFile="/home/yumani/software/LDAP/apacheds-1.5.7_new/external_keystore/carbon_server.jks"
            certificatePassword="secret">

After this restart Apache DS server. In my case will restart it via /etc/init.d (/etc/init.d/ldap restart;)

After the restart we can verify the connections using an LDAP browser. Lets use ApacheDirectoryStudio-linux-x86_64-1.5.2.v20091211.

Step 8:
WSO2 Carbon based products use Java Secure Socket Extension (JSSE) for SSL support. So we need to upgrade JAVA_HOME/jre/lib/security with JSSE provider.

To ensure G-Reg trusts the certificate used by the LDAP server we must install them in G-REGs trust store. So lets export the certificate using keystore.
keytool -export -keystore carbon_server.jks -alias carbon_server -file carbon_server.cer
Enter keystore password: 
Certificate stored in file <carbon_server.cer>

Then lets import this certificate to G-Reg's trust store like this:
 keytool -import -file carbon_server.cer -alias carbon_server -keystore /home/yumani/Documents/support/320/JPMDEVSPRT-31/wso2greg-4.0.0/repository/resources/security/client-truststore.jks -storepass wso2carbon
Owner: CN=yumani, OU=qa, O=wso2, L=col, ST=western, C=sl
Issuer: CN=yumani, OU=qa, O=wso2, L=col, ST=western, C=sl
Serial number: 4eba06d3
Valid from: Wed Nov 09 10:21:31 IST 2011 until: Tue Feb 07 10:21:31 IST 2012
Certificate fingerprints:
     MD5:  EB:23:58:74:3B:6A:1B:CC:26:D8:84:AE:D3:A5:AC:4D
     SHA1: 7F:73:3C:5B:BA:0B:B8:47:69:1E:12:5C:47:EB:D0:E9:C3:08:2E:AB
     Signature algorithm name: SHA1withRSA
     Version: 3
Trust this certificate? [no]:  yes
Certificate was added to keystore


Step 9:
Now that we have configured the LDAP server we need to setup and add user entries to the directory server. The guide (http://fusesource.com/docs/broker/5.3/security/LDAP-AddUserEntries.html) will show you how to complete this task.

Step 10:
Once the user store is setup we need to configure user manager in WSO2 G-Reg to connnect to it. This is how you do that:
Enable following block in G-REG_HOME/repository/conf/usermgt.xml.
        <!--UserStoreManager class="org.wso2.carbon.user.core.ldap.ApacheDSUserStoreManager">
            <Property name="ReadOnly">false</Property>
            <Property name="ConnectionURL">ldap://localhost:10389</Property>
            <Property name="ConnectionName">uid=admin,ou=system</Property>
            <Property name="ConnectionPassword">secret</Property>
            <Property name="passwordHashMethod">SHA</Property>
            <Property name="UserNameListFilter">(objectClass=person)</Property>
            <Property name="UserEntryObjectClass">inetOrgPerson</Property>
            <Property name="UserSearchBase">ou=system</Property>
            <Property name="UserNameSearchFilter">(&amp;(objectClass=person)(uid=?))</Property>
            <Property name="UserNameAttribute">uid</Property>
            <Property name="PasswordJavaScriptRegEx">[\\S]{5,30}</Property>
            <Property name="ReadLDAPGroups">true</Property>
            <Property name="WriteLDAPGroups">true</Property>
            <Property name="EmptyRolesAllowed">false</Property>
            <Property name="GroupSearchBase">ou=system</Property>
            <Property name="GroupNameListFilter">(objectClass=groupOfNames)</Property>
            <Property name="GroupEntryObjectClass">groupOfNames</Property>
            <Property name="GroupNameSearchFilter">(&amp;(objectClass=groupOfNames)(cn=?))</Property>
            <Property name="GroupNameAttribute">cn</Property>
            <Property name="MembershipAttribute">member</Property>
        </UserStoreManager-->

If you had user store configured as [1] you will need to update "UserSearchBase" property to include ou=users as well:
     <Property name="UserSearchBase">ou=users,ou=system</Property>

 
Final step:
Alright we have everything set, lets start WSO2 G-Reg server and acces the user store.

Start carbon server as below;
wso2server.sh -Djavax.net.ssl.trustStore=/path/to/carbon/repository/resources/security/client-truststore.jks -Djavax.net.ssl.trustStorePassword=wso2carbon

Access G-Reg admin console from https://localhost:9443/


Login to the server using admin credentials (admin/admin) as specified in usermgmt.xml

You may navigate to Configure > Users and Roles > Users and see that the user store is connected!!!

Featured

Selenium - Page Object Model and Action Methods

  How we change this code to PageObjectModel and action classes. 1 2 3 driver . findElement ( By . id ( "userEmail" )). sendKeys (...

Popular Posts