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
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.
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:
$ manifold export -f json -p my-production-app
{
"API_URL": "https://www.example.com"
}
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:
"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:
$ 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:
cy.request('POST', `${Cypress.env('API_URL')}/login`, {
username: 'Jane',
password: 'SuperSecret'
})
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
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:
$ 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:
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:
$ 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.
This imports a sibling commands.js
, which starts out empty. Mine, however, looks like this:
import { actions } from "../../src/api";Cypress.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:
Let’s test the case where the user is logged in, since the other case doesn’t need any environment variables.
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:
- You can use the Manifold CLI to inject secrets into any process.
- You can create custom resources to store collections of secrets, like those that you might need for a test suite.
- You can use
babel-plugin-transform-define
to translate environment variable references into values at compile time. - 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
- Essential best practices video by the creators of Cypress
- Complete Cypress documentation
- Amazing workshop by Kent C. Dodds teaching Cypress testing and a whole lot more
- Great post by Tim Speed using Manifold CLI with Docker