Dealing with Basic Auth in Cypress Tests

Max Rosin

Cypress is an open source testing framework that is described by its maintainers as "Fast, easy and reliable testing for anything that runs in a browser." I started to use it in the CI/CD pipeline of this blog to ensure that the website still works after I made changes. After a few days it has already proven itself to be powerful and easy to use. The only issue I had so far came up when I tried to run my tests against a development site behind basic auth. The primary issue is that the returned URL of cy.url() does not contain the basic auth credentials that are defined in the baseUrl configuration. It took me a moment to figure out why and searching the internet wasn't too helpful so I decided to write it down, so other people (or future me) may find the solution easier in the future.

The baseUrl

Cypress has a configuration option baseUrl which is used as a prefix in tests that use cy.visit() or cy.requests(). This option can be set in the configuration file. For example, if we set the following in our cypress.json file, we can use cy.visit("/") instead of cy.visit("http://localhost:8000/") in out tests.

{
  "baseUrl": "http://localhost:8000"
}

Overriding the baseUrl in the CI/CD

During local development and during initial tests in the CI/CD pipeline Cypress should run against a local server, so http://localhost:8000 is the correct default setting for our cypress.json file. But there are situations where it is useful to change the baseUrl for a Cypress run. For example in the pipeline of my blog I run the same Cypress tests twice. First against a local (local in the pipeline) instance of the blog which is just started by running gatsby develop. When all tests succeed the pipeline builds a Docker image and deploys it to dev.fotoallerlei.com. Then a second Cypress run kicks of, but this time against the deployed version on dev.fotoallerlei.com instead of a local one. To tell Cypress the different baseUrl we can set an environment variable for the pipeline job:

CYPRESS_BASE_URL="https://dev.fotoallerlei.com:8000"

Dealing with Basic Auth

The instance that is deployed to dev.fotoallerlei.com is almost identical to the production version on fotoallerlei.com. Almost? There is one small difference: The dev version is running behind a basic auth. This way it can be used to run tests against it and to verify changes manually in a setup that behaves identical to the production version. So, to run Cypress against the dev version we need to tell Cypress to send proper credentials. We can achive this by setting:

CYPRESS_BASE_URL="https://username:password@dev.fotoallerlei.com:8000"

And just like that, Cypress passes the credentials to the server and runs all tests as expected. Though, there is an edge case that does not work well with basic auth. Let's take a look at the following test:

describe("The Home Page", () => {
  it("go to next page", () => {
    cy.visit("/")
    cy.get("a.button.large.next").click()
    cy.url().should("eq", Cypress.config().baseUrl + "/blog/2")
  })
})

It visits /, meaning the root of our defined baseUrl, searches for the next button to go to the next page, clicks the button and then verifies that we really ended up on the next page. To pass the test independently of the baseUrl we use Cypress.config().baseUrl to get the current one. This works fine when we run the test with a local server or the productions version at fotoallerlei.com without basic auth. But as soon as we run it against dev.fotoallerlei.com with basic auth the test fails:

1. The Home Page
   go to next page:

   Timed out retrying

   - expected - actual

   -'https://dev.fotoallerlei.com/blog/2'
   +'https://username:password@dev.fotoallerlei.com/blog/2'

It looks like Cypress.config().baseUrl is returning the full expected baseUrl including the basic auth credentials but cy.url() only returns https://dev.fotoallerlei.com/blog/2... without the credentials. Obviously, these are not equal and the test fails. Why is that? If we check the documentation of cy.visit() we find a note:

Cypress will automatically attach this header at the network proxy level, outside of the browser. Therefore you will not see this header in the Dev Tools.

Cypress sends all its traffic through a proxy to manipulate the traffic if necessary. Apparently Cypress offloads the basic authentication to this proxy as well, which completely hides it from the testrun and therefore cy.url(). If we want to compare the expected URL with the actual URL we need an alternative to Cypress.config().baseUrl that returns the baseUrl without the basic auth credentials. Let's create a new Javascript file, in the default directory structure of Cypress cypress/support/base_url.js is a good start:

export const baseUrl = Cypress.config().baseUrl.replace(
  /(http.:\/\/).*:.*@(.*)$/,
  "$1$2"
)

export default baseUrl

The regex may look a bit scary at first. It takes the http:// or https:// and everything after the @ of the URL and puts them into a new constant baseUrl — or in other words: it removes username:password@ from our original URL. From now on we can use this value instead of querying Cypress.config().baseUrl and can always be sure that there will be no basic auth credentials in it. We only have to make sure to import it first.

import baseUrl from "../support/base_url.js"

describe("The Home Page", () => {
  it("go to next page", () => {
    cy.visit("/")
    cy.get("a.button.large.next").click()
    cy.url().should("eq", baseUrl + "/blog/2")
  })
})

Now the test runs through without any issues... with or without basic auth.