tldr;
Many Angular apps require the user to be authenticated before using the app. This can make writing and running Cypress end to end tests difficult. If your login flow requires you to leave the app to authenticate, the problem becomes even more difficult. This article will cover a solution to this problem and allow you to write your Cypress tests. The exact specifics of doing this will vary for each app, but each case should follow closely to the steps provided here:
- Create a login Cypress command
- Optionally, you can obtain a token from your auth server, and save it to localStorage (or sessionStorage, depending on your app)
- Run the login command in the
beforeEach
hook
The app I implemented this in was using the angular-oauth2-oidc
package for authentication, and our auth server is Identity Server 4.
Cypress Login Command
The first step is to create a Cypress command that we can run to make the app believe the user is logged in. You can either use a fake token if you don't plan on calling the API or you can get a real token from the auth server. Below is an example of getting the token from the auth server and storing it in localStorage
. You may have to save the token in another location for your app.
// authentication.commands.ts
declare namespace Cypress {
interface Chainable {
/**
* Custom command to setup login
* @example cy.login()
*/
login(): Chainable<Element>;
}
}
Cypress.Commands.add('login', () => {
const options = {
method: 'POST',
url: Cypress.env('authServerTokenUrl'),
body: {
client_id: Cypress.env('client_id'),
client_secret: Cypress.env('client_secret'),
grant_type: Cypress.env('grant_type'),
username: Cypress.env('username'),
password: Cypress.env('password'),
},
form: true,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
};
cy.request(options).then((response) => {
window.localStorage.setItem(
'id_token_claims_obj',
JSON.stringify({ /* userInfo object */ }),
);
window.localStorage.setItem('access_token', response.body.access_token);
window.localStorage.setItem('id_token', response.body.access_token);
window.localStorage.setItem('expires_at', '' + new Date().getTime() + 10 * 60 * 1000);
});
});
Let's walk through the function together. The top portion of the code block, starting with declare namespace Cypress
, declares the login method as part of the Cypress object. If you don't declare the login method like this calling cy.login()
will result in an error. The next portion of the code block, starting with Cypress.Commands.add
, adds the new login
command to be used in your Cypress tests. An options
object is created for making a call to the auth server to get a token. The request is made, and because it's a promise we can chain a .then
method on to the request and get the returned data inside the callback. In my case, the token comes back on the response.body
object as the access_token
. Our app uses localStorage
for storing the token so I store the token on window.localStorage
. I also set the expires_at
value and the user's info object. You may want to chain a .catch
method on to the request where you can catch errors and log what happens. This will help you debug if the request fails.
All these values may have different keys in your application, but you'll likely need to set each value. I figured out which values were needed by logging in to the app and opening localStorage
. I was able to see which values the auth package set in localStorage
and then by process of elimination found the ones that were required for the user to be authenticated when the app loads.
If you want to use a fake token, you can remove everything from the login
command except where the localStorage
values are set. This method will work just fine if you don't plan on calling the application's API for any data.
Use the login
Command
The next step is to actually use the login
command you created. There are a couple of ways to do this, but the best way is to call the command in the beforeEach
hook of your spec file.
// home.spec.ts
describe('Home', () => {
beforeEach(() => {
cy.login();
// other test setup
})
// e2e tests
})
After I first created the login
command I tried calling it in a before
hook instead of beforeEach
. That worked for the first test in the spec file, but localStorage
is cleared by Cypress after each test. Because of that the app was no longer authenticated after the first test. Thus, make sure to call the login command in the beforeEach hook.
Cypress Environment Variables
Cypress has multiple ways that you can provide environment variables to use in your tests. There are a lot of ways to provide the environment variables you need to request the token, but I used the cypress.env.json
file when running the tests on my local machine and provided the values on the command line when running the tests in our CI/CD pipeline. The cypress.env.json
file is not committed to our git repo so that the secrets are not accidentally made public.
Conclusion
It took me a few hours to figure out the specifics of how to make the app believe the user was logged in, but this method works perfectly. We can mock all the data from the API and use a fake token, or we can use a real token and call our actual API
Top comments (1)
Cypress is amazing really starting to use it more alongside other testing libraries like Jest.