Sam SlotskyEngineering @ Manifold

Secure secrets in Cypress with the Manifold CLI

You’ve probably heard of Cypress, the popular end-to-end testing tool. It seems like everyone is adopting it these days, and with good reason: it’s much faster, more powerful, and easier to use than similar tools that predate it. If you use Cypress, you might have already encountered the need to reference secrets from your test suite, and spent considerable time determining how to share those secrets easily and securely.

On the other hand, perhaps you haven’t heard of Manifold, but if you have, then you know that secrets management is a central aspect of what we do. We recently adopted Cypress for our dashboard site, and before long we found ourselves in this very position: needing to reference secrets from our test code. We take dogfooding quite seriously, so naturally, we wanted to use our own tools to achieve this.

The case for kibbles

Looks like dogfood to me!Looks like dogfood to me!

There could be any number of reasons for using secrets in your tests, and doing so safely and predictably is of utmost importance. Our case might be among the most common: we need secrets for logging in test users for different end-to-end testing scenarios.

Using the API to login is a great shortcut and is highly recommended as a best practice for Cypress testing. It’s much faster and simpler than the alternative of writing a script that logs in through the UI. But you might imagine that to support this in different environments, some things should be configurable, like the API URL or authentication credentials for seeded user accounts. We are using secrets to represent both of these, and we need to keep them secure and shareable. Dogfood to the rescue!

Buckets of secrets

The best place to store these shared secrets is inside of what we call a custom resource. Usually, a resource will represent a cloud service that your application integrates with, and will allow you to store API keys or any other sensitive information within. But we can also create a custom resource and treat it as a bucket of shared secrets that aren’t necessarily tied to a cloud service. The screenshot below shows one such custom resource on the Manifold Dashboard.

A custom resource on the Manifold Dashboard.A custom resource on the Manifold Dashboard.

You can then use the Manifold CLI to access your secrets in a couple different ways. For example, I can print them in JSON format:

1$ manifold export -f json -p my-production-app
2{
3 "API_URL": "[https://www.example.com](https://www.example.com)"
4}

*Note: I’m using the -p argument to specify a project name, but you can also do this in a .manifold.yml file! Try creating one with manifold init and modifying it so that the -p arg isn’t needed!*

For our own applications, the most common use case is probably manifold run. This takes another command as an argument, and it sets all of your secrets as environment variables before executing that command. As an example, if you had a parcel application, the start task might look like this:

1"start": "manifold run yarn parcel index.html"

Running this task sets the previously defined API_URL into my environment before running my bundler. With a parcel app, I don’t have to do anything else; process.env.API_URL will be automatically set in my application code. But with a Cypress suite, there are a couple more steps needed before you can reference your secrets from within a test.

Into the Woods

There is no shortage of options when it comes to setting environment variables in Cypress, but most approaches don’t fit our use case, mainly because they require us to know the secrets or to keep them on our file systems. In most cases, the manifold export command will work great with the cypress.env.json approach:

1$ manifold export -f json -p my-production-app > cypress.env.json

This is convenient, but now you must remember to add this file to .gitignore to avoid checking secrets into source control. Using manifold run is much more ephemeral in nature so we prefer this method. Furthermore, while using a cypress.env.json will usually fit the bill, it doesn’t actually work for testing our Dashboard site, and it’s worth explaining why before I show you how.

Bucking Best Practices

We can use Cypress to call our API directly using cy.request(). Normally you’d see something like this:

1cy.request('POST', `${Cypress.env('API_URL')}/login`, {
2 username: 'Jane',
3 password: 'SuperSecret'
4})

The cy.request() function is convenient and isn’t bound by the limitations of the browser, so it tends to be the preferred approach for making web requests directly from your test suite. We’re going to go against the community standard in this case, because our authentication involves some client side crypto that we don’t want to replicate. That means that we’re going to use our application code to login.

Because our API client relies on environment variables, we need our secrets injected into Cypress in a way that makes them available to the application code as well. That means that Cypress.env() is out of the question; we need to use process.env instead. To make this work smoothly, we’ll learn how to add plugins to a custom Babel configuration in Cypress, and we’ll also learn how to add custom Cypress commands.

More Than Meets The Eye

All this Babel about transformation is making me thirsty…All this Babel about transformation is making me thirsty…

First we need to pick a tool that’s capable of the kind of compile-time variable replacement that we need. One common way to do this is with babel-plugin-transform-define, but if you follow the examples and try to hook it up in some .babelrc file, it won’t make it into Cypress, which uses browserify to build your test suite and has its own build configuration.

Instead, we need to hook into the preprocessor API. Specifically, we can use the browserify preprocessor to modify the default Babel settings! Install the new dependencies first:

1$ yarn add @cypress/browserify-preprocessor babel-plugin-transform-define

The first time you run Cypress, it will create a new file where you’ll plugin to the preprocessor API. It’s located atcypress/plugins/index.js, and when you’re done with it, it should look something like this:

Embedded content: https://gist.github.com/sslotsky/d85c49f00b6f72b7299176111ed9e191

This allows us to reference process.env.API_URL from our Cypress tests! All we have to do is make sure to run our test suite with the Manifold CLI:

1$ manifold run -p my-production-app yarn cypress open

Getting the secrets into Cypress is the hard part, and we’re done with it! The rest is downhill, and the next stop is only for convenience.

Your wish is my custom command

Our plugin file isn’t the only thing that Cypress auto-generates for us. We also have a support/index.js which initially looks something like what we see below.

Auto-generated support files that let us customize Cypress behavior.Auto-generated support files that let us customize Cypress behavior.

This imports a sibling commands.js, which starts out empty. Mine, however, looks like this:

1import { actions } from "../../src/api";
2
3Cypress.Commands.add("login", actions.login);

This maps the cy.login function to actions.login from our API client. This function takes an email address and a password, so now I can call cy.login(email, password) from my Cypress tests. So let’s write one!

All together now

So what should we test? How about something simple. As a user, if I’m logged in, I want to see a greeting that’s just for me. Otherwise I want to see the login form. Here’s perhaps the simplest representation of this in a React component:

Embedded content: https://gist.github.com/sslotsky/eae524e8ab6e0fb044e5330212a874d6

Let’s test the case where the user is logged in, since the other case doesn’t need any environment variables.

Embedded content: https://gist.github.com/sslotsky/732e644b86c9e751c5da51e21128af55

As you can see, we use a before hook to call our cy.login command as a setup step. Then we verify that our email address appears in a greeting. The environment variables are injected through the preprocessor API and our test passes without a hitch. Now we can ride the Gravy Train all the way through the Great Cypress Forest. Life is savory.

That’s all, folks

Quick breakdown of what we just learned:

  1. You can use the Manifold CLI to inject secrets into any process.

  2. You can create custom resources to store collections of secrets, like those that you might need for a test suite.

  3. You can use babel-plugin-transform-define to translate environment variable references into values at compile time.

  4. You can use @cypress/browserify-preprocessor to run your Cypress code with a custom Babel configuration.

Together, these capabilities make it possible to securely inject shared secrets into your Cypress test suite. If you’re into that sort of thing.

Relevant stuff to check out

Stratus Background
StratusUpdate

Sign up for the Stratus Update newsletter

With our monthly newsletter, we’ll keep you up to date with a curated selection of the latest cloud services, projects and best practices.
Click here to read the latest issue.