huridocs/uwazi

View on GitHub
app/api/utils/specs/handleError.spec.js

Summary

Maintainability
A
0 mins
Test Coverage
import { legacyLogger } from 'api/log';
import { createError } from 'api/utils';

import { errors as elasticErrors } from '@elastic/elasticsearch';
import { S3TimeoutError } from 'api/files/S3Storage';
import { appContext } from 'api/utils/AppContext';
import util from 'node:util';
import { handleError, prettifyError } from '../handleError';

const contextRequestId = '1234';

const { ConnectionError } = elasticErrors;

describe('handleError', () => {
  beforeEach(() => {
    jest.resetAllMocks();
    jest.spyOn(legacyLogger, 'error').mockImplementation(() => {});
    jest.spyOn(legacyLogger, 'debug').mockImplementation(() => {});
    jest.spyOn(appContext, 'get').mockReturnValue(contextRequestId);
  });

  describe('errors by type', () => {
    describe('and is instance of S3TimeoutError', () => {
      it('should be a debug logLevel and a 408 http code', () => {
        const errorInstance = new S3TimeoutError(new Error('timeout'));
        const error = handleError(errorInstance);
        expect(error).toMatchObject({
          code: 408,
          logLevel: 'debug',
        });
        expect(legacyLogger.debug.mock.calls[0][0]).toContain('timeout');
      });
    });
    describe('when error is instance of Error', () => {
      it('should return the error with 500 code without the original error and error stack', () => {
        const errorInstance = new Error('error');

        const error = handleError(errorInstance);

        expect(error.code).toBe(500);
        expect(error.requestId).toBe(contextRequestId);
        expect(error.prettyMessage).toEqual('error');
        expect(error.message).toBeUndefined();
        expect(error.original).toBeUndefined();
      });

      it('should correctly log the original error for ElasticSearch exceptions', () => {
        const error = new ConnectionError('test error', { meta: 'some meta' });
        handleError(error);

        expect(legacyLogger.error).toHaveBeenCalledWith(
          `requestId: ${contextRequestId} \n${util.inspect(error)}
original error: {
 "name": "ConnectionError",
 "meta": {
  "meta": "some meta"
 }
}`,
          {}
        );
      });

      it('should log the error with the requestId', () => {
        const error = new Error('error');
        handleError(error);

        expect(legacyLogger.error).toHaveBeenCalledWith(
          `requestId: ${contextRequestId} \n${error.stack}\noriginal error: {}`,
          {}
        );
      });
    });

    describe('when error is created with createError', () => {
      it('should return the error', () => {
        const error = handleError(createError('test error', 400));
        expect(error).toMatchSnapshot();
      });

      it('should not log the error when code is not 500', () => {
        handleError(createError('test error', 400));
        expect(legacyLogger.error).not.toHaveBeenCalled();

        handleError(createError('test error'));
        expect(legacyLogger.error).toHaveBeenCalledWith(
          `requestId: ${contextRequestId} \ntest error`,
          {}
        );
      });
    });

    describe('when error is a MongoError', () => {
      it('should return the error with a 500 code', () => {
        const error = handleError({ name: 'MongoError', message: 'error', code: '345' });
        expect(error.code).toBe(500);
        expect(error.message).toBe('error');
      });
    });

    describe('when error is a mongoose ValidationError', () => {
      it('should return the error with a 422 error', () => {
        const error = handleError({ name: 'ValidationError', message: 'error', code: '1000' });
        expect(error.code).toBe(422);
        expect(error.message).toBe('error');
      });
    });
  });

  describe('when error is undefined', () => {
    it('should return generate a new error with code 500', () => {
      const error = handleError();
      expect(error.code).toBe(500);
      expect(error.prettyMessage).toMatch(/Unexpected error has occurred/i);
      expect(error.requestId).toBe(contextRequestId);
    });
  });

  describe('when error is uncaught', () => {
    it('should append the info into the message', () => {
      const uncaught = true;
      const error = handleError({ message: 'error' }, { uncaught });
      expect(error.message).toBe(
        'uncaught exception or unhandled rejection, Node process finished !!\n error'
      );
    });
  });

  describe('when "Cast to objectId failed"', () => {
    it('should set code to 400', () => {
      const error = handleError({ message: 'Cast to ObjectId failed for value' });
      expect(error.code).toBe(400);
    });
  });

  describe('when "rison decoder error"', () => {
    it('should set code to 400', () => {
      const error = handleError({ message: 'rison decoder error' });
      expect(error.code).toBe(400);
    });
  });

  describe('when error is 400', () => {
    it('should log it using debugLog', () => {
      handleError(createError('test error', 400));
      expect(legacyLogger.debug.mock.calls[0][0]).toContain('test error');
    });

    describe('and is instance of Error', () => {
      it('should include the original error', () => {
        const error = new Error('test error');
        error.name = 'Original error';
        handleError(createError(error, 400));
        expect(legacyLogger.debug.mock.calls[0][0]).toContain('Original error');
      });
    });
  });

  describe('when the body contains the user and password', () => {
    it('should not show them in the log', () => {
      handleError(createError('test error', 400), {
        req: { body: { username: 'admin', password: '1234' } },
      });
      expect(legacyLogger.debug.mock.calls).toMatchSnapshot();
    });
  });

  describe('when error has ajv validation errors', () => {
    it('should prettify the message', () => {
      const errorInstance = {
        ajv: true,
        code: 400,
        message: 'hello',
        validations: [
          {
            instancePath: 'a property',
            message: 'an error',
          },
        ],
      };
      const error = handleError(errorInstance);

      expect(error.code).toBe(400);
      const expectedPrettymessage = 'hello\na property: an error';
      expect(error.prettyMessage).toEqual(expectedPrettymessage);
    });
  });
});

describe('handleError without context', () => {
  it('should append a tenant error message to the original error', () => {
    jest.restoreAllMocks();
    jest.spyOn(legacyLogger, 'error').mockImplementation(() => {});
    const error = handleError(new Error('original error message'));
    expect(error.prettyMessage).toEqual('original error message');
    expect(legacyLogger.error).toHaveBeenCalledWith(
      expect.stringMatching(
        /\nError: original error message[\w\W]*Accessing nonexistent async context/
      ),
      {}
    );
  });
});

describe('prettifyError', () => {
  describe('when the error does not fall into any other category, and the resulting message would be empty', () => {
    it('should return JSON representation of the original error object as a message.', () => {
      const prettied = prettifyError({
        json: {},
        status: '404',
        headers: 'some_headers',
      });

      expect(prettied.prettyMessage).toBe(
        '{\n  "json": {},\n  "status": "404",\n  "headers": "some_headers"\n}'
      );
    });
  });
});