Introduction
If you have an interest in Browser Test Automation, you have probably heard about the rising popularity of Cypress. I have enthusiastically joined the bandwagon after many years of being underwhelmed by Selenium.
That being said, Cypress is a young tool compared to Selenium and there remain some open questions about some of the fine points of applying Cypress to APEX (if you’re new to APEX, learn more about it here). In this post, I will be specifically exploring how to bypass the APEX login for unit testing.
Why should we bypass the APEX login for unit testing?
According to the Cypress best practices, using my login UI before each test is an anti-pattern: “Do not use your UI to login before each test.” To summarize the reasoning in my own words, the chief advantages of bypassing the login UI are:
- Speed up your unit tests, where every second counts
- Avoid adding extraneous sources of failure to your unit tests – i.e. don’t test your login UI unecessarily
Applying this best-practice with Oracle APEX may not be self-evident because APEX requires a lot more than simply your username and password to authenticate your session (this complexity plausibly makes the authentication process more secure). I kicked off my exploration into this topic with a post to Stack Overflow, which, as of the time of this writing, did not get a definitive response. After further research into the subject, I’ve come away with some suggestions : 3 methods for bypassing the APEX login for unit testing
Quick aside: Some particulars on the mechanics of APEX authentication
Before we launch into the 3 methods, let’s take a moment to discuss how the APEX browser authentication process works. Beyond your username and password, APEX further requires a series of time-sensitive tokens to authenticate you. The full list of these tokens may change from one APEX version to the next and may also be responsive to the eccentricities of your authentication scheme.
You can confirm what parameters your authentication scheme requires by consulting the HTTP POST request made to the wwv_flow.accept address (using tamper data or recording your login using Apache Jmeter. I’ve made a video about recording your login through Jmeter here. )
From my observations, a successful authentication requires that I provide APEX with:
- A valid session id (or instance id)
- A page submission id
- My username and password (of course)
- And a page items protected id
After a successful login, all further navigation through your APEX application depends on just 2 things:
- A valid session
- A valid cookie
Method 1: login through the GUI (but only once)
This is the most intuitive method and it doesn’t require that you change any of your APEX / database configuration. Then again, it relies on using your login GUI which may be slow, at least by the standards of unit testing.
In the following cypress script, I take the following steps:
- I login to my APEX login GUI in a ‘before’ hook.
- In this same hook, I extract the resulting url (with authenticated session id) and cookie.
- In all subsequent unit tests, I programmatically set the cookie and manipulate the authenticated url to match my intended destination.
context('Method 1: Unit testing APEX', () => { const loginPage = 'localhost/ords/f?p=100:LOGIN_DESKTOP' const pUsername = 'test_user' const pPassword = 'Oradoc_db1' var loggedInPage var app_cookie var pageUrl before(function() { cy.intercept('POST', 'ords/wwv_flow.accept').as('login') cy.visit(loginPage) cy.clearCookie('LOGIN_USERNAME_COOKIE') cy.get('[data-cy=username]') .clear() .should('be.empty') .type(pUsername) .should('have.value',pUsername) cy.get('[data-cy=password]') .should('be.empty') .type(pPassword) .should('have.value',pPassword) cy.get('[data-cy=sign_inButton]').click() cy.wait('@login') cy.url().should('contain', ':1:') .then(($url) => { loggedInPage = $url }) cy.getCookie('ORA_WWV_APP_100').then(($Cookie) => { app_cookie = $Cookie.value }) }) it('Visit page 2', () => { cy.setCookie('ORA_WWV_APP_100', app_cookie) pageUrl = loggedInPage.replace(':1:',':2:') cy.visit(pageUrl) }) it('Visit page 3', () => { cy.setCookie('ORA_WWV_APP_100', app_cookie) pageUrl = loggedInPage.replace(':1:',':3:') cy.visit(pageUrl) }) })
(note: don’t be confused by the ‘data-cy’ references – I make a practice of adding these attributes to all the page elements I interact with using Cypress, as per another best practice.)
Edit : Per the comments below this blog post – using Cypress.Cookies.preserveOnce(‘ORA_WWV_APP_100’) is a more elegant approach that allows you to dispense with set the app_cookie variable and then setting the cookie in each unit test. However, if you are switching pages in every unit test, you may find that the less elegant approach (the above) is more reliable.
Method 2: Getting session and cookie with PL/SQL
This is the most elegant and speediest solution of the 3 but requires that someone with SYS access to grant restricted access to a very sensitive APEX view.
2.1 Create a view to access session id and cookie
Your test user only needs a valid session id and cookie to authenticate a session. This information is available in a highly sensitive table called wwv_flow_sessions$ in your APEX schema. To avoid any risks associated with selecting from this view, I propose that you create a view that only displays this data for the username you use to test your application and grant it to a non-privileged schema.
create or replace view test_user_cookie as select id as app_session, cookie_value, username from apex_180100.wwv_flow_sessions$ where username ='TEST_USER' and workspace_user_id is null; / grant select on test_user_cookie to cypress_user;
2.2 Rest enable your schema
Your goal to make this data available to your Cypress code so you have rest enable your non-privileged schema.
begin ORDS.ENABLE_SCHEMA(p_enabled => TRUE, p_schema => 'CYPRESS_USER', p_url_mapping_type => 'BASE_PATH', p_url_mapping_pattern => 'cypress_user', p_auto_rest_auth => FALSE); commit; end;
2.3 Prepare a module, template and get handler
As your now rest-enabled user, create a module, template and get handler to display the requisite session id and cookie value.
begin ORDS.DEFINE_MODULE( p_module_name => 'mysession', p_base_path => '/mysession', p_items_per_page => 25, p_status => 'PUBLISHED', p_comments => NULL ); commit; end; /
begin ORDS.DEFINE_TEMPLATE( p_module_name => 'mysession', p_pattern => 'test_user/', p_priority => 0, p_etag_type => 'HASH', p_etag_query => NULL, p_comments => NULL ); commit; end; /
Prepare your GET handler to run a PL/SQL block:
Your GET handler should read out your test user’s cookie and session id value after creating a session if one doesn’t already exist:
declare l_count_session number; l_app_session number; l_cookie_value varchar2(50); begin select count(*) into l_count_session from sys.test_user_cookie; if l_count_session = 0 then apex_session.create_session ( p_app_id => 100, p_page_id => 1, p_username => 'TEST_USER' ); end if; select app_session, cookie_value into l_app_session, l_cookie_value from sys.test_user_cookie; apex_json.open_object; apex_json.write('app_session',l_app_session); apex_json.write('cookie_value',l_cookie_value); apex_json.close_object; end;
2.4 Test your web service
Test your RESTful service in your browser by adapting the following link: http://localhost/32181/ords/cypress_user/mysession/test_user/
2.5 Use your RESTful service in your Cypress code
You can now retrieve the output of your RESTful service by using cy.request. In the following cypress script, I take the following steps:
- I fetch an authenticated session id and cookie value in a ‘beforeEach’ hook
- In all subsequent unit tests, I merely assemble my intended url using the authenticated session id
describe('Method 2: Unit testing APEX', function() { var app_100_cookie var valid_session var url = 'http://localhost:32181/ords/f?p=100:' var authUrl beforeEach(function() { cy.request('http://localhost:32181/ords/cypress_user/mysession/mycookie/').then((response) => { console.log(response) valid_session = response.body.app_session console.log(valid_session) app_100_cookie = response.body.cookie_value console.log(app_100_cookie) cy.setCookie('ORA_WWV_APP_100', app_100_cookie) }) }) it('visit home page', function() { authUrl = url + '1:' + valid_session cy.visit(authUrl) }) it('visit page 2', function() { authUrl = url + '2:' + valid_session cy.visit(authUrl) }) it('visit page 3', function() { authUrl = url + '3:' + valid_session cy.visit(authUrl) }) })
N.B. : Make sure you don’t deploy this configuration to Production.
Method 3: Use a ‘No Authentication’ ‘Switch in Session’ scheme
A 3rd solution is to apply an authentication scheme in your Development environment that doesn’t require authentication. The constraint is you still want an authentication scheme in your Production environment. I propose enabling a ‘Switch in Session’ ‘No Authentication’ scheme to which you apply a build to prevent deployment to Production. If the unit test you are looking to perform has authentication as a dependency, you can further add, say, a Post-Authentication Procedure Name to programmatically login.
3.1 Create a ‘No Authentication’ authentication scheme in your Development environment
Don’t opt for the ‘make current’ option for this 2nd authentication schema. You are not replacing your existing authentication scheme.
Enable ‘Switch in session’ for this authentication scheme.
3.2 Prevent this 2nd authentication scheme from being used in Production
You don’t want this authentication scheme accidentally deployed to production. The following code will check whether a build option named ‘DEV_ONLY is set to ‘include’. If it is not set to ‘include’, it will not enable this authentication scheme.
function allow_alt_auth return boolean is begin return apex_util.get_build_option_status( P_APPLICATION_ID => :APP_ID, P_BUILD_OPTION_NAME => 'DEV_ONLY') = 'INCLUDE'; end;
This code pairs with the following build, which you’ll need to add to your Development environment:
Putting this all together, in the following cypress script, I take the following steps:
- I visit the login page with the APEX_AUTHENTICATION parameter in the url in a ‘beforeEach’ hook
- In all subsequent unit tests, I can visit any page in the application I need so long as I continue to include the APEX_AUTHENTICATION parameter in the url.
describe('Method 3: Unit testing APEX', function() { var url = 'localhost/ords/f?p=100:LOGIN_DESKTOP::APEX_AUTHENTICATION=noauth' beforeEach(function() { cy.visit(url) }) it('visit home page', function() { cy.visit('localhost/ords/f?p=100:1::APEX_AUTHENTICATION=noauth::::') }) it('visit page 2', function() { cy.visit('localhost/ords/f?p=100:2::APEX_AUTHENTICATION=noauth::::') }) it('visit page 3', function() { cy.visit('localhost/ords/f?p=100:3::APEX_AUTHENTICATION=noauth::::') }) })
Conclusion
Thanks for reading. The above list of suggested approaches to unit testing APEX is by no means exhaustive and hopefully more and better approaches emerge over time. Can you think of other ways to bypass the APEX login? Please let me know if you have any questions about how to implement any of this.
In Method-1, it says local host. Can we make it to work for Azure CI/CD??
Hi – are you attempting to build your APEX environment from scratch in your CI/CD? If you are having trouble with this, you may consider simply referencing the public link to the website you are testing, if that’s an option.
I suspect example 1 can be simplified by just preserving the cookie before each “test”. This is the approach we use and it allows us to remain logged in across “it” blocks.
beforeEach(() => {
Cypress.Cookies.preserveOnce(‘ORA_WWV_APP_100’);
});
I wonder. Instead of having to build a REST API just to expose the cookie data in example 2, your could just use the Oracle NodeJS driver and access these directly. This way, your tests will be much more independent.
I like the sound of this I’m not sure I completely follow. Are you suggesting I retrieve the session and cookie from wwv_flow_sessions$ view through the NodeJS driver? I will give that a try. What do you mean by ‘more independent’?
You could write a plugin based on the official Oracle NodeJS driver that lets you issue queries directly to the database with a cy.oracle() command. It means that any DB interaction required takes place directly in the test itself.
So, in your case, you could just issue a query on a view to retrieve the session data you require. This makes the test independent as you aren’t having to manually build anything outside of the test itself (REST API in your case).
In our tests we do a lot of data manipulation/verification in our Cypress tests using this method – we even set up our user prior to logging in and strive for a predictable static data set meaning our tests can run on any environment without external configuration.
Thanks for clarifying. It makes sense to me. I will give this a try.
Agreed. That is a simpler way to set the cookie. Thanks for the recommendation. I will update the blog to reflect this.
Hi Hayden,
Great webinar yesterday. How did you fix the error that cypress insert “__” into url ? What version of chrome you used ?
Have you ever used testCafe ? Take a look on page 31 https://apex.oracle.com/pls/apex/f?p=111708:31.
Thank you,
Fabio Eloy
Hi Fabio – thanks for attending my webinar.
For your 1st question, I think the answer I gave on this github issue should give you the broader context and some ideas for possible solutions: https://github.com/cypress-io/cypress/issues/3975#issuecomment-552241159. The bug is an inconvenience but should not cause any real issues, from my perspective.
TestCafe looks intriguing. I just installed it and am playing around with it. Certainly, it has the advantage over Cypress that it works with more browsers. However, TestCafe doesn’t have the left-hand logging panel that makes Cypress so easy to debug. Also, it seems that TestCafe is quite a bit slower. Are you a TestCafe user? What has your experience been?
Hi Hayden,
I am just curious to hear your experience with Cypress and testing Interactive Grids. I have an application that has many Interactive Grids and was struggling with the best way to automate testing.
Thanks
Vito
Hi Vito – great question. I didn’t get to IGs in my webinar but I will consider covering them in future talks. Here’s a recent example of a Cypress test for IG that I’ve written: https://github.com/forallabeautifulearth/fabe-browser-tests/blob/master/cypress/integration/admin/app201.p210.admin.js Perhaps it will address any questions that you have.
Do you have e book for this ???