12

I'm starting to test my application using Jest and Supertest (for endpoints). Tests work smoothly but Jest detects 2 open handles after running tests which prevents Jest from exiting cleanly.

This open handles are generated by an external async function that is being called within my test file. I'm using an external function to request a JWT Token from Auth0 API; but that request to Auth0 also provides in it's response crucial information to pass the endpoint's middlewares (more info about this below). Two things to have in mind here:

  1. So far, I can't avoid requesting a token from Auth0 because that response, as I said, also includes a user object with key information. Auth0 sets this object outside of the body response, at that same level, but not within it. That information is key to pass the endpoint's middleware.
  2. I've isolated all the errors to be sure that the problem shows up only when I call the external async function that requests from Auth0 API's the token and user info; the issue is generated by using that function (called getToken) within the test file.

Test file code

import app from "../app";
import mongoose from "mongoose";
import supertest from "supertest";
import { getToken } from "../helpers";
import dotenv from "dotenv";
import * as config from "../config";

dotenv.config();

const api = supertest(app);

let authToken: any;
let db: any;

beforeAll(async() => {
  try {
    mongoose.connect(config.MONGODB_URI, {
      useNewUrlParser: true,
      useUnifiedTopology: true,
      useCreateIndex: true,
    });
    db = mongoose.connection;
    db.on("error", console.error.bind(console, "Console Error:"));
    db.once("open", () =>
      console.log(`App connected to "${db.name}" database`)
    );
    authToken = await getToken()
  } catch (err) {
    return err
  }
});

describe("GET /interview/:idCandidate", () => {
  test("With auth0 and read permissions", async () => {
       await api
        .get("/interview/1")
        .set("Authorization", "Bearer " + authToken)
        .expect(200)
  });
});

afterAll(async () => {
  try {
    await db.close();
  } catch (err) {
    return err;
  }
});

getToken external function that requests info to Auth0 API

The getToken function that is imported from external module is as follows:

import axios from 'axios'

var options = {
    url: //url goes here,
    form:
    {
      // form object goes here
    },
    json: true
  };
  
  const getToken = async () => {
    try {
      const tokenRequest = await axios.post(options.url, options.form)
      return tokenRequest.data.access_token
    } catch (err){
      return err
    }
  } 


export default getToken;

Issue

Once my tests are run, they run as expected until Jest's --detectOpenHandles configuration detects the two following open handles:

Jest has detected the following 2 open handles potentially keeping Jest from exiting:

  ●  TLSWRAP

      60 |             case 0:
      61 |                 _a.trys.push([0, 2, , 3]);
    > 62 |                 return [4 /*yield*/, axios_1.default.post(options.url, options.form)
         |                                                      ^
      63 |                 ];  
      64 |             case 1:    

      at RedirectableRequest.Object.<anonymous>.RedirectableRequest._performRequest (node_modules/follow-redirects/index.js:265:24)
      at new RedirectableRequest (node_modules/follow-redirects/index.js:61:8)
      at Object.request (node_modules/follow-redirects/index.js:456:14)
      at dispatchHttpRequest (node_modules/axios/lib/adapters/http.js:202:25)
      at httpAdapter (node_modules/axios/lib/adapters/http.js:46:10)
      at dispatchRequest (node_modules/axios/lib/core/dispatchRequest.js:53:10)
      at Axios.request (node_modules/axios/lib/core/Axios.js:108:15)
      at Axios.<computed> [as post] (node_modules/axios/lib/core/Axios.js:140:17)
      at Function.post (node_modules/axios/lib/helpers/bind.js:9:15)
      at call (dist/helpers/getToken.js:62:54)
      at step (dist/helpers/getToken.js:33:23)
      at Object.next (dist/helpers/getToken.js:14:53)
      at dist/helpers/getToken.js:8:71
      at __awaiter (dist/helpers/getToken.js:4:12)
      at Object.token (dist/helpers/getToken.js:56:34)
      at call (dist/test/api.test.js:87:48)
      at step (dist/test/api.test.js:52:23)
      at Object.next (dist/test/api.test.js:33:53)
      at dist/test/api.test.js:27:71
      at __awaiter (dist/test/api.test.js:23:12)
      at dist/test/api.test.js:72:32


  ●  TLSWRAP

      141 |             switch (_a.label) {
      142 |                 case 0: return [4 /*yield*/, api
    > 143 |                         .get("/interview/1")
          |                          ^
      144 |                         .set("Authorization", "Bearer " + authToken)
      145 |                         .expect(200)];
      146 |                 case 1:

      at Test.Object.<anonymous>.Test.serverAddress (node_modules/supertest/lib/test.js:61:33)
      at new Test (node_modules/supertest/lib/test.js:38:12)
      at Object.get (node_modules/supertest/index.js:27:14)
      at call (dist/test/api.test.js:143:26)
      at step (dist/test/api.test.js:52:23)
      at Object.next (dist/test/api.test.js:33:53)
      at dist/test/api.test.js:27:71
      at __awaiter (dist/test/api.test.js:23:12)
      at Object.<anonymous> (dist/test/api.test.js:139:70)

I'm certain that the error is coming from this getToken async function.

Why am I not mocking the function?

You might be wondering why am I not mocking that function and as I said before, when Auth0 responds with the token (which refreshes quite often by the way), it also responds with info regarding the user, and that info goes outside the response.body. As a matter of fact, it goes at the same hierarchical level as the body. So, if I you wanted to mock this function, I would have to set the Authorization header with the bearer token on one side (which is easy to do with Supertest), and the user info provided by Auth0 on the other side; but this last step is not possible (at least as far as I know; otherwise, how do you set a user info property at the same hierarchy level as the body and not within it?)

Things I've tried

I've tried adding a longer timeout to the test and to beforeAll(); I've tried adding the done callback instead of using async/await within beforeAll() and some other not very important things and none of them solves the open handle issue. As a matter of fact, I've checked if the request process to Auth0 API is closed after the response and effectively, that connection closes but I still get open handle error after running the tests.

Any idea would be highly appreciated!

4 Answers4

6

I've been also struggling with a similar problem today and failed to find a definite solution, but found a workaround. The workaround (posted by alfreema) is to put the following line before you make a call to axios.post:

await process.nextTick(() => {});

This seems to allow Axios to complete its housekeeping and be ready to track new connections opened afterwards. This is just my speculation, I hope someone else can shed more light on it and provide a proper solution.

RocketR
  • 3,626
  • 2
  • 25
  • 38
  • This do the job correctly – waloar May 03 '22 at 22:33
  • 2
    Does anyone know why is this working? I placed that before calling the function under test(the one calling axios) and I do not get errors anymore... – Javier Guzmán May 10 '22 at 16:53
  • 1
    Damn, you must be a magician – CreativeJoe May 19 '22 at 12:19
  • Can somebody explain why does this work? – mdanishs Oct 09 '22 at 15:04
  • `process.nextTick` doesn't return a promise. `await 1234` will have the same effect. – dipea Feb 20 '23 at 13:37
  • 1
    @dipea Thanks. Looks like I've inadvertently spread nonsense. At least it helped people get past their issue. – RocketR Feb 21 '23 at 14:48
  • Thanks @RocketR. This worked for me `await Promise.resolve(process.nextTick(Boolean));` **Explanation**: _The `nextTick()` method is not a `Promise`, it returns `void`. BUT in JS your proposed solution works, however in TS not. So basically I wrapped the `nextTick()` call into a `Promise`, and it works like a charm._ – jherax Aug 13 '23 at 04:06
2

Thanks @RocketR, it's really work.

await process.nextTick(() => { });
const newData = await axios.post(`${url}`, pattern);
const token = await axios.post(`${url}/token`, tokenData,
    { 
        headers: { 
            'Content-Type': 'application/x-www-form-urlencoded' 
        } 
    });
YauheniN
  • 21
  • 1
2

Every call I will call await process.nextTick(() => {});.

Example:

await process.nextTick(() => {});
await supertest(app)
  .get("/api/getBooking")
  .set({ Authorization: `Bearer ${TOKEN}` })
  .then((response) => {
    expect(response.statusCode).toBe(200);
  })
Tyler2P
  • 2,324
  • 26
  • 22
  • 31
0

It seems that there is an odd behavior with Mongoose and an internal call to nextTick. I solved the problem as proposed here, by adding a nextTick() before calling the supertest POST.

afterAll(async () => {
  await dbMock.closeDatabase();
  server.close();
});

it(`POST "${v1}/products" responds with the entity created`, async () => {
    const payload: IProduct = { /* ... */ };

    // odd fix for Jest open handle error
    await Promise.resolve(process.nextTick(Boolean));

    const reply = await request(server).post(`${v1}/products`).send(payload);

    expect(reply.statusCode).toEqual(200);
});

As the process.nextTick() method does not return a Promise but a void, I wrapped the call inside a Promise.resolve(), and now it becomes awaitable.

jherax
  • 5,238
  • 5
  • 38
  • 50