14Jun
3 methods to bypass the login when unit testing APEX with Cypress.io
By: Hayden Hudson On: June 14, 2019 In: APEX Developer Solutions Comments: 13

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:

  1. Speed up your unit tests, where every second counts
  2. 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:

  1. I login to my APEX login GUI in a ‘before’ hook.
  2. In this same hook, I extract the resulting url (with authenticated session id) and cookie.
  3. 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:

  1. I fetch an authenticated session id and cookie value in a ‘beforeEach’ hook
  2. 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:

  1. I visit the login page with the APEX_AUTHENTICATION parameter in the url in a ‘beforeEach’ hook
  2. 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.

Want more? I’ve created a Github repo to get you set up. Subscribe to our blog now so you never miss a thing, and instantly receive a functional APEX application with associated Cypress scripts to illustrate the above concepts.

Share this:
Share

13 Comments:

    • Apj585
    • September 19, 2019
    • Reply

    In Method-1, it says local host. Can we make it to work for Azure CI/CD??

      • Hayden Hudson
      • September 19, 2019
      • Reply

      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.

    • David Lawton
    • November 04, 2019
    • Reply

    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’);
    });

      • David Lawton
      • November 04, 2019
      • Reply

      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.

        • Hayden Hudson
        • November 04, 2019
        • Reply

        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’?

          • David Lawton
          • November 05, 2019
          • Reply

          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.

            • Hayden Hudson
            • November 06, 2019

            Thanks for clarifying. It makes sense to me. I will give this a try.

      • Hayden Hudson
      • November 04, 2019
      • Reply

      Agreed. That is a simpler way to set the cookie. Thanks for the recommendation. I will update the blog to reflect this.

    • Fabio Eloy
    • December 12, 2019
    • Reply

    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

      • Hayden Hudson
      • December 12, 2019
      • Reply

      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?

    • Vito
    • December 15, 2019
    • Reply

    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

    • Marion Reishus
    • April 01, 2021
    • Reply

    Do you have e book for this ???

Leave reply:

Your email address will not be published. Required fields are marked *