Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Async external function leaves open handles - Jest, Supertest, Express

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!

like image 384
Mauro Consolani Avatar asked Sep 13 '21 21:09

Mauro Consolani


1 Answers

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.

like image 146
RocketR Avatar answered Sep 27 '22 18:09

RocketR