J. Hesters
J. Hesters

Reputation: 14768

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

Upvotes: 7

Views: 6612

Answers (2)

J. Hesters
J. Hesters

Reputation: 14768

Steve's answer actually helped me to end up with this code in cypress/plugins/index.ts.

import dotenv from 'dotenv';

dotenv.config({ path: '.env.local' });

import { encryptSession } from 'utils/sessions';

/**
 * @type {Cypress.PluginConfig}
 */
const pluginConfig: Cypress.PluginConfig = (on, config) => {
  on('task', {
    encryptSession: (session: {
      issuer: string;
      publicAddress: string;
      email: string;
    }) => encryptSession(session),
  });
};

export default pluginConfig;

Then in cypress/support/commands.ts.


Cypress.Commands.add(
  'loginWithCookie',
  ({
    issuer = 'some-issuer',
    publicAddress = 'some-address',
    email = 'some-email',
  } = {}) => {
    const session = { issuer, publicAddress, email };

    return cy.task<string>('encryptSession', session).then(token => {
      return cy.setCookie('my-secret-token', token).then(() => {
        return session;
      });
    });
  },
);

Upvotes: 3

user14783414
user14783414

Reputation:

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.

Upvotes: 4

Related Questions