How to use Jest to test functions using crypto or window.msCrypto

56.3k views Asked by At

When running unit tests with Jest in react the window.crypto API is causing problems. I haven't found a way to incorporate crypto in Jest without installing other packages which is something I can't do. So without using another npm package is there a way to test functions that use: crypto.getRandomValues() in them that doesn't crash Jest? Any links, advice, or tips are appreciated

19

There are 19 answers

8
Hardik Modha On BEST ANSWER

Use the following code to set up the crypto property globally. It will allow Jest to access

  • window.crypto in the browser environment
  • global.crypto in non-browsers environments. (Node/Typescript scripts).

It uses the globalThis which is now available on most of the latest browsers as well as Node.js 12+

const crypto = require('crypto');

Object.defineProperty(globalThis, 'crypto', {
  value: {
    getRandomValues: arr => crypto.randomBytes(arr.length)
  }
});
0
Exit196 On

I have this problem in Angular 8 with Jest tests for lib that are using uuid generator. In jest test setup i mock this:

Object.defineProperty(global.self, 'crypto', {
  value: {
    getRandomValues: arr => arr
  },
});
2
user42488 On

Building upon what others suggested here, I resolved the issue with window.crypto.subtle.digest with the following:

Object.defineProperty(global.self, "crypto", {
  value: {
    getRandomValues: (arr: any) => crypto.randomBytes(arr.length),
    subtle: {
      digest: (algorithm: string, data: Uint8Array) => {
        return new Promise((resolve, reject) =>
          resolve(
            createHash(algorithm.toLowerCase().replace("-", ""))
              .update(data)
              .digest()
          )
        );
      },
    },
  },
});

Or, if not using Typescript:

Object.defineProperty(global.self, "crypto", {
  value: {
    getRandomValues: (arr) => crypto.randomBytes(arr.length),
    subtle: {
      digest: (algorithm, data) => {
        return new Promise((resolve, reject) =>
          resolve(
            createHash(algorithm.toLowerCase().replace("-", ""))
              .update(data)
              .digest()
          )
        );
      },
    },
  },
});

The reformating of the string is optional. It is also possible to hardcode the algorithm, e.g. by stating 'sha256' or 'sha512' or alike.

0
Obiwahn On

Deriving from AIVeligs answer:

Since I use "node" environment in Jest I had to use

module.exports = {
  preset: "ts-jest",
  testEnvironment: "node",
  globals: {
    crypto: {
      getRandomValues: (arr) => require("crypto").randomBytes(arr.length),
    },
  },
};
3
marinona21 On

I'm using vue-jest, and what worked for me is the following configuration in jest.config.js file:

module.exports = {
   ...
   setupFiles: [
      '<rootDir>/tests/settings/jest.crypto-setup.js',
   ],
};

and in jest.crypto-setup.js:

global.crypto = { 
     getRandomValues: (arr) => require('crypto').randomBytes(arr.length) 
};

Adding the getRandomValues function definition directly in module.exports didn't work since the globals object must be json-serializable (as it is specified here: https://jestjs.io/docs/configuration#globals-object).

2
Michael On

For nodeJS + typescript, just use global instead of global.self

import crypto from 'crypto'

Object.defineProperty(global, 'crypto', {
  value: {
    getRandomValues: (arr:any) => crypto.randomBytes(arr.length)
  }
});
0
Fi Li Ppo On

late to the party, but I usually do something like:

// module imports here
// important: the following mock should be placed in the global scope

jest.mock('crypto', function () {
  return {
    randomBytes: jest
      .fn()
      .mockImplementation(
        () =>
          'bla bla bla'
      ),
  }
});

describe('My API endpoint', () => {
  it('should work', async () => {
    const spy = jest.spyOn(DB.prototype, 'method_name_here');
    // prepare app and call endpoint here
    expect(spy).toBeCalledWith({ password: 'bla bla bla' });
  });
});

0
Kay Plata On
const crypto = require('crypto');
global.crypto = crypto;
0
Victor van Poppelen On

The polyfills in the current answers are incomplete, since Crypto.getRandomValues() modifies its argument in-place as well as returning it. You can verify this by running something like const foo = new Int8Array(8); console.log(foo === crypto.getRandomValues(foo)) in your browser console, which will print true.

getRandomValues() also does not accept an Array as its argument, it only accepts integer TypedArrays. Node.js' crypto.randomBytes() function is not appropriate for this polyfill, as it outputs raw bytes, whereas getRandomValues() can accept signed integer arrays with elements up to 32 bits. If you try crypto.getRandomValues(new Int32Array(8)) in your browser, you might see something like [ 304988465, -2059294531, 229644318, 2114525000, -1735257198, -1757724709, -52939542, 486981698 ]. But if you try node -e 'console.log([...require("crypto").randomBytes(8)])' on the command line, you might see [ 155, 124, 189, 86, 25, 44, 167, 159 ]. Clearly these are not equivalent, and your component under test might not behave as expected if tested with the latter.

The latest versions of Node.js solve this problem with the webcrypto module (should be a matter of setting globalThis.crypto = require('crypto').webcrypto). If you're using an older version of Node (v14 or below) you might have better luck using crypto.randomFillSync(), which should be useable as a drop-in replacement for getRandomValues() as it modifies a passed buffer/TypedArray in-place.

In your Jest setup file (can't be set via the globals configuration as it only allows JSON-compatible values):

const { randomFillSync } = require('crypto')

Object.defineProperty(globalThis, 'crypto', {
  value: { getRandomValues: randomFillSync },
})
0
pom On

Since node 15.x you can use crypto.webcrypto

eg.

import crypto from "crypto";

Object.defineProperty(global.self, "crypto", {
  value: {
    subtle: crypto.webcrypto.subtle,
  },
});
1
Daniel On

The default crypto dependency didn't work for me during testing with Jest.

Instead I used the @peculiar/webcrypto library:

yarn add -D @peculiar/webcrypto

Then in your Jest setup file, just add this:

import { Crypto } from "@peculiar/webcrypto";


window.crypto = new Crypto();
0
Lin On

dspacejs's answer almost worked for me, except I had the same problem as Mozgor. I got an error saying that window.crypto is readonly. You can use Object.assign instead of directly trying to overwrite it.

Install @peculiar/webcrypto with yarn add -D @peculiar/webcrypto or npm i --save-dev @peculiar/webcrypto

Then add the following to your Jest setup file:

import { Crypto } from "@peculiar/webcrypto";

Object.assign(window, {
  crypto: new Crypto(),
})
1
Ulf Aslak On

In the default configuration, Jest assumes you are testing a Node.js environment. But when you are getting errors using methods of the window object, you are probably making a web app.

So if you are making a web app, you should use "jsdom" as your "testEnvironment". To do this, insert "testEnvironment": "jsdom", into your Jest configurations.

If you maintain a "jest.config.js" file, then add it like:

module.exports = {
   ...
   "testEnvironment": "jsdom",
   ...
};

Or if, like me, you keep the Jest configs in "package.json":

{
    ...,
    "jest": {
        ...,
        "testEnvironment": "jsdom",
        ...
    },
    ...
}
0
Tianzhen Lin On

If you need to use the randomUUID function from the crypto module in a Node.js environment where it might not be available (e.g., older Node.js versions), you can mock it using Jest. This can be particularly useful in testing scenarios. To achieve this, you can utilize a utility library like FakerJS to generate UUIDs.

Here's how you can set up the mock with Jest:

First, create a Jest setup file to include your mock:

// jest.setup.js

import { faker } from '@faker-js/faker';

// Mock `crypto.randomUUID` which may not be available in some NodeJS environments
crypto.randomUUID = () => faker.string.uuid();

Then, make sure to reference this setup file in your Jest configuration:

// jest.config.js

module.exports = {
  setupFilesAfterEnv: ['<rootDir>/path/to/jest.setup.js'], // Correct key to include setup file
  // Include the rest of your Jest configuration here
}

Note the correction in the FakerJS method to generate UUIDs (faker.datatype.uuid() instead of faker.string.uuid()), and the use of setupFilesAfterEnv instead of setupFiles in the Jest configuration to ensure the mock is applied correctly after all Jest environments have been set up.

This setup will allow you to use crypto.randomUUID() in your Jest tests as if it were natively supported in your Node.js environment, leveraging FakerJS to generate the UUIDs.

0
Dick Larsson On

Depency injection is one way to solve this.

Node.js provides an implementation of the standard Web Crypto API. Use require('node:crypto').webcrypto to access this module.

So you pass the crypto object to the code that depends on it.

Notice how we "inject" the correct crypto object when invoking the method utils.aesGcmEncrypt

test("Encrypt and decrypt text using password", async () => {
  const password = "Elvis is alive";
  const secret =
    "surprise action festival assume omit copper title fit tower will chalk bird";
  const crypto = require("crypto").webcrypto;
  const encrypted = await utils.aesGcmEncrypt(crypto, secret, password);
  const decrypted = await utils.aesGcmDecrypt(crypto, encrypted, password);

  expect(decrypted).toBe(secret);
});
1
ayZagen On

For the ones using jsdom (jest-environment-jsdom) environment with Jest >=28 you should define replacement module as a getter.

//jest.config.js
module.exports = {
  testEnvironment: "jsdom",
  rootDir: './',
  moduleFileExtensions: ['ts', 'js'],
  setupFilesAfterEnv: ["<rootDir>/test/setup-env.tsx"],
  preset: 'ts-jest',
};
// setup-env.tsx
const { Crypto } = require("@peculiar/webcrypto");
const cryptoModule = new Crypto();

Object.defineProperty(window, 'crypto', {
  get(){
    return cryptoModule
  }
})

I am using @peculiar/webcrypto but other implementations should work also.

1
Arihant Banthia On

I have implemented it using jest and it failed to execute after I upgraded jest version. Earlier I was using in this way :

global.crypto = {
 getRandomValues: jest.fn();
} 

After upgrade, it was failing. So I tried in this way :

global.crypto.getRandomValues = jest.fn();

and it worked fine.

1
AlVelig On

Add crypto global for your jest environment as if it were in browser. Your jest.config.js should look like:

const {defaults} = require('jest-config');

module.exports = {
  globals: {
    ...defaults.globals,
    crypto: require('crypto')
  }
};

Ref: https://jestjs.io/docs/en/configuration#globals-object

3
mitchelc On

Like @RwwL, the accepted answer did not work for me. I found that the polyfill used in this library did work: commit with polyfill

//setupTests.tsx
const nodeCrypto = require('crypto');
window.crypto = {
  getRandomValues: function (buffer) {
    return nodeCrypto.randomFillSync(buffer);
  }
};
//jest.config.js
module.exports = {
 //...
  setupFilesAfterEnv: ["<rootDir>/src/setupTests.tsx"],
};