Async external function leaves open handles - Jest, Supertest, Express
Asked Answered
W

4

13

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!

Weller answered 13/9, 2021 at 21:52 Comment(0)
P
9

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.

Previse answered 5/1, 2022 at 19:25 Comment(7)
This do the job correctlyHuntingdonshire
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...Quintin
Damn, you must be a magicianSupernal
Can somebody explain why does this work?Burger
process.nextTick doesn't return a promise. await 1234 will have the same effect.Twinkling
@Twinkling Thanks. Looks like I've inadvertently spread nonsense. At least it helped people get past their issue.Previse
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.Gitt
L
3

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' 
        } 
    });
Lorentz answered 10/4, 2022 at 20:52 Comment(0)
A
3

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);
  })
Analyst answered 29/12, 2022 at 11:52 Comment(0)
G
1

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.

Gitt answered 13/8, 2023 at 4:22 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.