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: 2

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

before(function() {
   cy.server()
   cy.route('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)
     var pageUrl = loggedInPage.replace(':1:',':2:')
     cy.visit(pageUrl)
    })

   it('Visit page 3', () => {
     cy.setCookie('ORA_WWV_APP_100', app_cookie)
     var 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.)

 

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.

Share this:
Share

2 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.

Leave reply:

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