I’m building a Next.js app and write my tests using Cypress. I configure the environment variables using a .env.local
file locally. In the CI pipeline, they are defined normally.
I’m trying to write a custom command in Cypress that encrypts a session in cypress/support/command.ts
.
import { encryptSession } from 'utils/sessions'; Cypress.Commands.add( 'loginWithCookie', ({ issuer = 'some-issuer', publicAddress = 'some-address', email = 'some-mail', } = {}) => { const session = { issuer, publicAddress, email }; return encryptSession(session).then(token => { cy.setCookie('my-session-token', token); return session; }); }, );
When this command runs, it fails because encryptSession
uses a TOKEN_SECRET
environment variable, that Cypress doesn’t load.
import Iron from '@hapi/iron'; const TOKEN_SECRET = process.env.TOKEN_SECRET || ''; export function encryptSession(session: Record<string, unknown>) { return Iron.seal(session, TOKEN_SECRET, Iron.defaults); }
How can I get Cypress to load the environment variables from that file, if its there (= only locally because the variables are defined in the CI – it should detect the other variables in the pipeline normally, so the equivalent of detecting a variable that has been set with export MY_VAR=foo
)?
Answer
There is Cypress.env, but you want to set the token on process.env
which looks like it’s not fully coordinated with the Cypress version.
I know that any process.env
with a key with prefix of CYPRESS_ ends up in Cypress.env()
, but you want to go in the opposite direction.
I would use a task which gives you access to the file system and process.env
,
/cypress/plugins/index.js
module.exports = (on, config) => { on('task', { checkEnvToken :() => { const contents = fs.readFileSync('.env.local', 'utf8'); // get the whole file const envVars = contents.split('n').filter(v => v); // split by lines // and remove blanks envVars.forEach(v => { const [key, value] = v.trim().split('='); // split the kv pair if (!process.env[key]) { // check if already set in CI process.env[key] = value; } }) return null; // required for a task }, })
Call the task ahead of any tests, either in /cypress/support/index.js, or a before()
, or in the custom command.
In the custom command
Cypress.Commands.add( 'loginWithCookie', ({ issuer = 'some-issuer', publicAddress = 'some-address', email = 'some-mail', } = {}) => { cy.task('checkEnvToken').then(() => { // wait for task to finish const session = { issuer, publicAddress, email }; return encryptSession(session).then(token => { cy.setCookie('my-session-token', token); return session; }); }) });
Digging into the code for @hapi/iron
, there is a call to crypto
which is a Node library, so you may need to move the whole encryptSession(session)
call into a task to make it work.
import { encryptSession } from 'utils/sessions'; module.exports = (on, config) => { on('task', { encryptSession: (session) => { const contents = fs.readFileSync('.env.local', 'utf8'); // get the whole file const envVars = contents.split('n').filter(v => v); // split by lines // and remove blanks envVars.forEach(v => { const [key, value] = v.trim().split('='); // split the kv pair if (!process.env[key]) { // check if already set in CI process.env[key] = value; } }) return encryptSession(session); // return the token }, })
Call with
cy.task('encryptSession', { issuer, publicAddress, email }) .then(token => { cy.setCookie('my-session-token', token); });
Where to run the above cy.task
I guess you only need to run it once per test session (so that it’s set for a number of spec files) in which case the place to call it is inside a before()
in /cypress/support/index.js.
The downside of placing it there is it’s kind of hidden, so personally I’d put it inside a before()
at the top of each spec file.
There’s a small time overhead in the fs.readFileSync
but it’s minimal compared to waiting for page loads etc.