DEV Community

Preston Lamb
Preston Lamb

Posted on • Originally published at prestonlamb.com on

Authenticating Your App for Cypress Tests

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);
    });
});
Enter fullscreen mode Exit fullscreen mode

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
})
Enter fullscreen mode Exit fullscreen mode

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

Discussion (1)

Collapse
andrewbaisden profile image
Andrew Baisden

Cypress is amazing really starting to use it more alongside other testing libraries like Jest.