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
)?
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.