I'm creating an Express API and everything was going ok untill I started to have some problems with tests. I use Jest and Supertest to tests the endpoints and I don't know why I started to have this message when I run the tests.
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)
I'm using Redis for caching and rate limiting and I know that it has to be realted to it but I don't know how to fix it. These errors only occur with endpoint tests. The most wierd thing is that these error only show if I run tests with VSCode open, when I run test from an external terminal and VSCode closed all the tests pass.
I have run test with --detectOpenHandles to see where is the problem and when I have VSCode open the outpout is:
Segmentation fault (core dumped)
with VSCode closed is:
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)
I have tried to use jest hooks to close Redis connections after all tests, before all tests, and after and before each tests in several ways I have found but it didn't work.
afterAll(async () => {
await new Promise((resolve) => {
redis.quit();
redis.on('end', resolve);
});
});
or
afterAll(async () => {
await redis.quit();
});
I have reinstall VSCode, Node.js, used different version of Node, run npm rebuild, unistall jest extension of VSCode, deleted node_modules and npm install after but nothing works.
These are my files:
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;
};
routes.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);
ingredients.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);
ingredients.controller.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