Búsqueda de sitios web

node.js - La suite de pruebas de Jest no pudo ejecutarse: 'Fallo de segmentación (volcado de núcleo)' o 'Un proceso trabajador de Jest (pid=xxx) fue terminado por otro proceso'


Estoy creando una API con Express y todo iba bien hasta que empecé a tener algunos problemas con las pruebas. Uso Jest y Supertest para probar los endpoints y no sé por qué empecé a recibir este mensaje cuando ejecuto las pruebas.

 FAIL  src/modules/ingredients/tests/updateingredient.test.ts
  ● Test suite failed to run

    A jest worker process (pid=672269) was terminated by another process: signal=SIGSEGV, exitCode=null. Operating system logs may contain more information on why this occurred.

      at ChildProcessWorker._onExit (node_modules/jest-worker/build/workers/ChildProcessWorker.js:366:23)

(node:672290) MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 open listeners added to [_0x323dee]. Use emitter.setMaxListeners() to increase limit
(Use `node --trace-warnings ...` to show where the warning was created)

Estoy usando Redis para caching y limitación de tasa y sé que tiene que estar relacionado con eso, pero no sé cómo solucionarlo. Estos errores solo ocurren con las pruebas de los endpoints. Lo más extraño es que estos errores solo aparecen si ejecuto las pruebas con VSCode abierto; cuando ejecuto las pruebas desde una terminal externa y VSCode cerrado, todas las pruebas pasan.

He ejecutado la prueba con --detectOpenHandles para ver dónde está el problema y cuando tengo VSCode abierto, la salida es:

Segmentation fault (core dumped) 

con VSCode cerrado es:

TCPWRAP

       5 | const redisDb = config.get<number>('redis_db');
       6 |
    >  7 | export const redis = new Redis({
         |                      ^
       8 |   port: 6379,
       9 |   host: '127.0.0.1',
      10 |   db: redisDb,

      at node_modules/ioredis/built/connectors/StandaloneConnector.js:44:21
      at StandaloneConnector.connect (node_modules/ioredis/built/connectors/StandaloneConnector.js:43:16)
      at node_modules/ioredis/built/Redis.js:121:64
      at EventEmitter.connect (node_modules/ioredis/built/Redis.js:103:25)
      at new Redis (node_modules/ioredis/built/Redis.js:77:18)
      at Object.<anonymous> (src/utils/redis.ts:7:22)
      at Object.<anonymous> (src/modules/allergens/tests/listAllergens.test.ts:5:1)

He intentado usar los hooks de Jest para cerrar las conexiones de Redis después de todas las pruebas, antes de todas las pruebas, y después y antes de cada prueba de varias maneras que he encontrado, pero no funcionó.

afterAll(async () => {
  await new Promise((resolve) => {
    redis.quit();
    redis.on('end', resolve);
  });
});

o

afterAll(async () => {
  await redis.quit();
});

He reinstalado VSCode, Node.js, usado diferentes versiones de Node, ejecutado npm rebuild, desinstalado la extensión de Jest de VSCode, eliminado node_modules y npm install después, pero nada funciona.

Estos son mis archivos:

redis.ts

import Redis from 'ioredis';
import config from 'config';
import { Logger } from 'src/logger/logger';

const redisDb = config.get<number>('redis_db');

export const redis = new Redis({
  port: 6379,
  host: '127.0.0.1',
  db: redisDb,
});

redis.on('ready', () => Logger.info('redis ready'));
redis.on('error', (error) => {
  Logger.error(`error with redis connection: ${error}`);
  return redis.disconnect();
});

rateLimiter.ts

import { NextFunction, Request, Response } from 'express';
import { redis } from 'src/utils/redis';

export interface Limiter {
  windowSize: number;
  allowedRequests: number;
}
export const rateLimiter =
  ({ windowSize, allowedRequests }: Limiter) =>
  async (req: Request, res: Response, next: NextFunction) => {
    const ip = (req.headers['x-forwarded-for'] || req.socket.remoteAddress) as string;

    const formatIp = (ipAddress: string) => {
      if (ipAddress.substring(0, 7) === '::ffff:') {
        return ipAddress.replace('::ffff:', '');
      }
      return ipAddress;
    };
    const formattedIp = formatIp(ip);

    const requests = await redis.incr(formattedIp);

    if (requests === 1) {
      await redis.expire(formattedIp, windowSize);
    } else {
      await redis.ttl(formattedIp);
    }

    if (requests > allowedRequests) {
      return res.status(429).send({ error: 'too many requests' });
    }
    return next();
  };

app.ts

import express from 'express';
import helmet from 'helmet';
import cors from 'cors';
import { httpLogger } from './logger/httpLogger';
import { Logger } from './logger/logger';
import { errorHandler, isTrustedError } from './error/error-handler';
import { apiRouter } from './routes';

export const createApp = () => {
  const app = express();

  app.use(helmet());
  app.use(cors());
  app.use(express.json());
  app.use(express.urlencoded({ extended: false }));
  app.use(httpLogger);

  app.use('/api/v1', apiRouter);

  app.use(errorHandler);

  process.on('uncaughtException', async (error: Error) => {
    Logger.error(error);
    if (!isTrustedError(error)) {
      process.exit(1);
    }
  });
  process.on('unhandledRejection', (reason: string) => {
    Logger.error(reason);
    process.exit(1);
  });

  return app;
};

rutas.ts

import { Request, Response, Router } from 'express';
import { rateLimiter } from './middlewares/rateLimiter';
import { allergensRouter } from './modules/allergens/allergens.router';
import { ingredientsRouter } from './modules/ingredients/ingredients.router';
import { recipesRouter } from './modules/recipes/recipes.routes';

export const apiRouter = Router();

apiRouter.get(
  '/health',

  (_req: Request, res: Response) => {
    const data = {
      uptime: process.uptime(),
      responseTime: process.hrtime(),
      message: 'ok',
      date: new Date(),
    };
    return res.status(200).send({ data });
  }
);
apiRouter.use(rateLimiter({ windowSize: 20, allowedRequests: 4 }));
apiRouter.use('/allergens', allergensRouter);
apiRouter.use('/ingredients', ingredientsRouter);
apiRouter.use('/recipes', recipesRouter);

ingredientes.router.ts

import { Router } from 'express';
import { validate } from 'src/middlewares/validationRequest';
import {
  deleteAllIngredients,
  deleteIngredientById,
  findIngredientById,
  findIngredients,
  makeIngredient,
  patchIngredient,
} from './ingredients.controller';
import {
  createIngredientSchema,
  deleteIngredientSchema,
  deleteIngredientByIdSchema,
  getIngredientByIdSchema,
  getIngredientSchema,
  updateIngredientSchema,
} from './ingredients.schema';
import { cache } from 'src/middlewares/cache.middleware';

export const ingredientsRouter = Router();

ingredientsRouter.get('/', validate(getIngredientSchema), cache, findIngredients);
ingredientsRouter.get('/:id', validate(getIngredientByIdSchema), cache, findIngredientById);

ingredientsRouter.post('/', validate(createIngredientSchema), makeIngredient);

ingredientsRouter.delete('/', validate(deleteIngredientSchema), deleteAllIngredients);
ingredientsRouter.delete('/:id', validate(deleteIngredientByIdSchema), deleteIngredientById);

ingredientsRouter.patch('/:id', validate(updateIngredientSchema), patchIngredient);

ingredientes.controlador.ts

import { NextFunction, Request, Response } from 'express';
import { filterProperties } from 'src/utils/filterProperties';
import { isValidId } from 'src/utils/idValidation';
import { ApiError } from '../../error/ApiError';
import { redis } from 'src/utils/redis';
import {
  getIngredients,
  getIngredientById,
  getIngredientsByAllergen,
  createIngredient,
  updateIngredient,
  removeIngredientById,
  removeAllIngredients,
} from './ingredients.service';

export const findIngredients = async (req: Request, res: Response, next: NextFunction) => {
  const { allergenNames } = req.query;

  const properties = ['name', 'category', 'hasAllergens', 'allergens', 'allergenNames'];

  const filteredQuery = filterProperties(properties, req.query);

  if (allergenNames) {
    const parsedNames = Array.isArray(allergenNames) ? allergenNames : [allergenNames.toString()];

    try {
      const foundIngredients = await getIngredientsByAllergen(parsedNames);

      redis.setex(`ingredients_allergen_${parsedNames}`, 3600, JSON.stringify(foundIngredients));

      return res.status(200).send({ data: foundIngredients });
    } catch (error) {
      return next(error);
    }
  }
  try {
    const foundIngredients = await getIngredients(filteredQuery);

    Object.keys(filteredQuery).length > 0
      ? redis.setex(
          `ingredients_${Object.keys(filteredQuery)}`,
          3600,
          JSON.stringify(foundIngredients)
        )
      : redis.setex('ingredients', 3600, JSON.stringify(foundIngredients));

    return res.status(200).send({ data: foundIngredients });
  } catch (error) {
    return next(error);
  }
};
... more code

listIngredients.test.ts

import * as IngredientsService from '../ingredients.service';
import { IngredientInput } from '../ingredients.model';
import { createApp } from '../../../app';
import { redis } from 'src/utils/redis';

const app = createApp();

const ingredientInput: IngredientInput = {
  name: 'ingredient 1',
  category: 'eggs',
  hasAllergens: true,
  allergens: ['639eea5a049fc933bddebab3'],
  allergenNames: ['celery'],
};

const ingredientPayload = {
  _id: '639eea5a049fc933bddebab2',
  name: 'ingredient 1',
  category: 'eggs',
  hasAllergens: true,
  allergens: ['639eea5a049fc933bddebab3'],
  allergenNames: ['celery'],
};

const baseApiUrl = '/api/v1/ingredients';

const getIngredientsServiceMock = jest.spyOn(IngredientsService, 'getIngredients');

const getIngredientsByAllergenServiceMock = jest.spyOn(
  IngredientsService,
  'getIngredientsByAllergen'
);

const getIngredientByIdServiceMock = jest.spyOn(IngredientsService, 'getIngredientById');

beforeEach(() => {
  jest.clearAllMocks();
  jest.resetAllMocks();
  redis.flushdb();
});

afterAll(async () => {
  await redis.quit();
});

... tests start here

Artículos relacionados