Cypress is an amazing end-to-end testing framework that can test anything that runs in a browser. If you haven't tried using it yet then I strongly recommend you to check it out. At Kablamo, we have been using Cypress since early 2020 and after working with many Selenium-based testing frameworks, I wouldn't want to use anything else unless of course there is a particular project need.
Cypress is built for Developers & QA alike, requires minimal setup, and makes it easy to write, run and debug tests. As of now, it supports a wide variety of major browsers and is constantly rolling out new features and functionalities. Our clients have also benefited because of said features, as we are able to run the automated test suite on-demand and in the CI/CD setup to prevent any bug leaks into production, with the biggest benefit of Cypress being its developer-friendly nature.
I first came across authenticating through Azure Active Directory in one of our recent projects, a quick google search revealed an open Cypress Github issue since early 2018. There were a few suggestions in there on how to overcome this, such as using Cypress Recipe for Logging in and this article from Auth0. However none of them worked for our setup and after trying and testing a few different approaches, it turned out to be a much simpler solution in the end.
Our Azure AD login setup is pretty basic: the user hits login from our application and is redirected
to https://login.microsoftonline.com/
using OAuth2.0, here the user enters their Active Directory email & password
combination to authenticate and gets redirected back in our application with a valid auth/access token set in localStorage.
I first thought of just creating the test purely via UI as an end-user would interact with the application. However, when visiting external sites Cypress strongly discourages testing via third-party UI, since your application is the system under test not the third-party app.
On top of that, if you still want to login via third-party UI you will need to compromise your browser
security by adding "chromeWebSecurity": false
in your configuration which is not ideal for security reasons,
and even then you are going through a third-party system that is completely outside your control and can contribute
to flakey tests down the road.
Ideally, we should programmatically test the integration between microsoftonline
and our application with
cy.request()
command. It is not only quicker but also more reliable and still gives us 100% confidence of
testing the functionality of our app. This Azure AD authentication
article
helped inspire me to create the solution implemented here, along with the help of my colleagues Nathan & Ian.
So without any further background here it goes:
Following best practices, I started out with writing a custom command to login via Azure AD in cypress/support/index.ts
file.
js// ------------------------------------------// Log in and set localStorage with auth key// ------------------------------------------Cypress.Commands.add('login', () => {cy.log('-- Login via Azure AD & Set Local Storage --');const options = {method: 'POST',url: `${Cypress.config('AUTH_URL')}` +`/${Cypress.config('AUTH_TENANT_ID')}/oauth2/v2.0/token`,form: true,body: {client_id: Cypress.config('AUTH_CLIENT_ID'),scope: Cypress.config('AUTH_SCOPE'),username: Cypress.config('AUTH_USERNAME'),password: Cypress.config('AUTH_PASSWORD'),grant_type: Cypress.config('AUTH_GRANT_TYPE'),},};cy.request(options).then((response) => {// here I am appending an extra type/value// to accomodate our app's localStorage requirementsresponse.body.type='azure';const authValue = JSON.stringify(response.body);window.localStorage.setItem('auth', authValue);})});
All Cypress.config()
values reside in the <root>/cypress.config
file, and all the Azure AD specific
values are found on your Azure AD dashboard or talk to your devOps specialist for details.
As I am using TypeScript
for my Cypress tests, I will also declare it in cypress/integration/index.ts
file.
ts/// <reference types="Cypress" />declare namespace Cypress {interface cy {login(): void;...}}
That's it, we are now ready to write our integration test as usual,
ts/// <reference types="Cypress" />describe('Dashboard test', () => {beforeEach(() => {cy.login(); // This will call our custom login functioncy.visit(/);...});it.only('Login and search', () => {...}}
I hope this helps inspire your own solution around it, of course there are a few articles on solving the same issue online, and as always depends on the setup of your application.
References: