node.jsexpressjestjssuperagentjose

jest fails with error: Cannot find module 'jose-node-cjs-runtime/jwt/sign'


I am testing my Node.js API using jest. This API is an Express app used for managing tasks. It has signup/login feature to allow only authenticated users to use the app. It has an endpoint for signing up new user and many endpoints are present which uses an express middleware to validate the user authentication through JWT. I have jose-node-cjs-runtime@^3.15.5 package installed which is used for JWT generation and validation.

If I run the project using dev script env-cmd -f ./config/dev.env nodemon src/index.js, there are no issues and it runs fine. I am trying to test user signup using jest test file which uses superagent package to test the endpoint. I am using test script env-cmd -f ./config/test.env jest --watch to run the test. This command is showing following error and test is failing:

 FAIL  tests/user.test.js
  ● Test suite failed to run

    Cannot find module 'jose-node-cjs-runtime/jwt/sign' from 'src/models/user.js'

    Require stack:
      src/models/user.js
      src/routers/user.js
      src/app.js
      tests/user.test.js

      3 | const bcrypt = require('bcryptjs');
      4 | // library for signing jwt
    > 5 | const { SignJWT } = require('jose-node-cjs-runtime/jwt/sign');
        |                     ^
      6 | // library for generating symmetric key for jwt
      7 | const { createSecretKey } = require('crypto');
      8 | // task model

      at Resolver.resolveModule (node_modules/jest-resolve/build/resolver.js:313:11)
      at Object.<anonymous> (src/models/user.js:5:21)

Test Suites: 1 failed, 1 total
Tests:       0 total
Snapshots:   0 total
Time:        3.385 s
Ran all test suites related to changed files.

I am unable to figure out why this error is caused. Please help me in finding the solution.

I was using Node.js version 16.7.0 and got this error. I have upgraded my Node.js version today. My current Node.js version is 16.9.0. After upgrading Node.js version also I am receiving this error. Following is the package.json file contents for the project:

{
  "name": "task-manager",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "engines": {
    "node": ">=16.7.0"
  },
  "scripts": {
    "start": "node src/index.js",
    "dev": "env-cmd -f ./config/dev.env nodemon src/index.js",
    "test": "env-cmd -f ./config/test.env jest --watch"
  },
  "jest": {
    "testEnvironment": "node",
    "roots": [
      "<rootDir>"
    ],
    "modulePaths": [
      "<rootDir>"
    ],
    "moduleDirectories": [
      "node_modules"
    ]
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "@sendgrid/mail": "^7.4.6",
    "bcryptjs": "^2.4.3",
    "express": "^4.17.1",
    "jose-node-cjs-runtime": "^3.15.5",
    "mongodb": "^4.1.1",
    "mongoose": "^6.0.2",
    "multer": "^1.4.3",
    "sharp": "^0.29.0",
    "validator": "^13.6.0"
  },
  "devDependencies": {
    "env-cmd": "^10.1.0",
    "jest": "^27.1.0",
    "nodemon": "^2.0.12",
    "supertest": "^6.1.6"
  }
}

Following is the contents of src/models/user.js:

const mongoose = require('mongoose');
const validator = require('validator');
const bcrypt = require('bcryptjs');
// library for signing jwt
const { SignJWT } = require('jose-node-cjs-runtime/jwt/sign');
// library for generating symmetric key for jwt
const { createSecretKey } = require('crypto');
// task model
const Task = require('./task');
...

Content of src/tests/user.test.js:

const request = require('supertest');
const app = require('../src/app');

describe('user route', () => {
  test('should signup a new user', async () => {
    await request(app)
      .post('/users')
      .send({
        name: '__test_name__',
        email: '__test_email__',
        password: '__test_password__',
      })
      .expect(201);
  });
});

Solution

  • You can use the resolver from this workaround.

    I have a test repo with jose being used in all kinds of tools here.

    Obviously your setup may vary if you use ts-jest, or ESM test files, or similar. But at that point you should be familiar with the different jest magical options.

    > jest --resolver='./temporary_resolver.js' 'jest.test.*'
    
     PASS  ./jest.test.js
      ✓ it works in .js (3 ms)
    
    Test Suites: 1 passed, 1 total
    Tests:       1 passed, 1 total
    Snapshots:   0 total
    Time:        0.367 s, estimated 1 s
    Ran all test suites matching /jest.test.*/i.
    
    // temporary workaround while we wait for https://github.com/facebook/jest/issues/9771
    
    const resolver = require('enhanced-resolve').create.sync({
      conditionNames: ['require'],
      extensions: ['.js', '.json', '.node', '.ts']
    })
    
    module.exports = function (request, options) {
      return resolver(options.basedir, request)
    }
    
    const { generateSecret } = require("jose/util/generate_secret");
    
    test('it works in .js', async () => {
      expect(typeof generateSecret).toBe('function');
      expect(await generateSecret('HS256')).toBeTruthy();
    });
    

    Bottom line - its about making jest require the cjs distribution of the jose module and using the exports mapping. Note that ESM is still not natively supported and neither is the exports mapping, both of which are stable for over a year in all LTS releases of node.