Search code examples
javascriptenvironment-variablescypressdotenv

Cypress load environment variables in custom commands


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


Solution

  • 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.