huridocs/uwazi

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

Summary

Maintainability
A
0 mins
Test Coverage
/**
 * @jest-environment jsdom
 */
import backend from 'fetch-mock';
import { APIURL } from 'app/config';
import { store } from 'app/store';
import api from 'app/utils/api';
import { RequestParams } from 'app/utils/RequestParams';
import loadingBar from 'app/App/LoadingProgressBar';
import * as notifyActions from 'app/Notifications/actions/notificationsActions';

const mockRedirect = jest.fn();

jest.mock('react-router-dom', () => ({
  ...jest.requireActual('react-router-dom'),
  redirect: jest.fn().mockImplementation(path => mockRedirect(path)),
}));

describe('api', () => {
  const validationErrorResponse = {
    status: 422,
    body: {
      error: 'validation failed',
      validations: [
        {
          instancePath: ".metadata['prop1']",
          message: 'should be string',
        },
        {
          instancePath: ".metadata['prop2']",
          message: 'is too long',
        },
      ],
    },
  };

  beforeEach(() => {
    spyOn(loadingBar, 'start');
    spyOn(loadingBar, 'done');
    spyOn(store, 'dispatch');
    spyOn(notifyActions, 'notify').and.returnValue('notify action');
    backend.restore();
    backend
      .get(`${APIURL}test_get`, JSON.stringify({ method: 'GET' }))
      .get(`${APIURL}test_get?key=value`, JSON.stringify({ method: 'GET' }))
      .post(`${APIURL}test_post`, JSON.stringify({ method: 'POST' }))
      .delete(`${APIURL}test_delete?data=delete`, JSON.stringify({ method: 'DELETE' }))
      .get(`${APIURL}badrequest`, { status: 400, body: { error: 'cannot process' } })
      .get(`${APIURL}unauthorized`, { status: 401, body: {} })
      .get(`${APIURL}notfound`, { status: 404, body: {} })
      .get(`${APIURL}conflict`, { status: 409, body: { error: 'conflict error' } })
      .get(`${APIURL}error_url`, { status: 500, body: { requestId: '1234' } })
      .get(`${APIURL}network_error`, {
        throws: new TypeError('Failed to fetch'),
      })
      .get(`${APIURL}unknown_error`, { throws: new Error('some error') })
      .post(`${APIURL}test_payload_too_large_error`, {
        status: 500,
        body: { error: 'PayloadTooLargeError: request entity too large at readStream' },
      })
      .post(`${APIURL}elastic_index_error`, {
        status: 500,
        body: { error: 'ERROR Failed to index documents: id1' },
      })
      .post(`${APIURL}validation_error`, validationErrorResponse)
      .post(`${APIURL}unprocessable_entity`, {
        status: 422,
        body: { error: 'unprocessable entity' },
      });
  });

  afterEach(() => {
    backend.restore();
    jest.clearAllMocks();
  });

  describe('GET', () => {
    it('should prefix url with config api url', async () => {
      const response = await api.get('test_get');
      expect(response.json.method).toBe('GET');
    });

    it('should start and end the loading bar', async () => {
      await api.get('test_get');
      expect(loadingBar.done).toHaveBeenCalled();
      expect(loadingBar.start).toHaveBeenCalled();
    });

    it('should add headers and data as query string', async () => {
      const requestParams = new RequestParams(
        { key: 'value' },
        { header: 'value', header2: 'value2' }
      );
      await api.get('test_get', requestParams);

      expect(backend.calls()[0][1].headers.header).toBe('value');
      expect(backend.calls()[0][1].headers.header2).toBe('value2');
    });
  });

  describe('POST', () => {
    it('should prefix url with config api url', async () => {
      const requestParams = new RequestParams(
        { key: 'value' },
        { header: 'value', header2: 'value2' }
      );
      const response = await api.post('test_post', requestParams);

      expect(backend.calls()[0][1].body).toBe(JSON.stringify({ key: 'value' }));
      expect(backend.calls()[0][1].headers.header).toBe('value');
      expect(backend.calls()[0][1].headers.header2).toBe('value2');
      expect(response.json.method).toBe('POST');
    });

    it('should start and end the loading bar', async () => {
      const promise = api.post('test_post', {});
      expect(loadingBar.start).toHaveBeenCalled();

      await promise;
      expect(loadingBar.done).toHaveBeenCalled();
    });
  });

  describe('DELETE', () => {
    it('should prefix url with config api url', async () => {
      const requestParams = new RequestParams(
        { data: 'delete' },
        { header: 'value', header2: 'value2' }
      );
      const response = await api.delete('test_delete', requestParams);

      expect(response.json.method).toBe('DELETE');
      expect(backend.calls()[0][1].headers.header).toBe('value');
      expect(backend.calls()[0][1].headers.header2).toBe('value2');
    });

    it('should start and end the loading bar', async () => {
      const requestParams = new RequestParams(
        { data: 'delete' },
        { header: 'value', header2: 'value2' }
      );
      const promise = api.delete('test_delete', requestParams);
      expect(loadingBar.start).toHaveBeenCalled();

      await promise;
      expect(loadingBar.done).toHaveBeenCalled();
    });
  });

  function testNotificationDisplayed(message, type = 'danger') {
    expect(store.dispatch).toHaveBeenCalledWith('notify action');
    expect(notifyActions.notify).toHaveBeenCalledWith(message, type);
  }

  async function testErrorHandling(endpoint, errorCallback) {
    try {
      await api.get(endpoint);
      fail('should throw error');
    } catch (e) {
      errorCallback(e);
    }
  }

  describe('error handling', () => {
    describe('400', () => {
      it('should notify as danger', async () => {
        await testErrorHandling('badrequest', () => {
          testNotificationDisplayed('cannot process', 'danger');
        });
      });
    });

    describe('401', () => {
      it('should redirect to login', async () => {
        await testErrorHandling('unauthorized', () => {
          expect(mockRedirect).toHaveBeenCalledWith('/login');
        });
      });
    });

    describe('404', () => {
      it('should redirect to login', async () => {
        await testErrorHandling('notfound', () => {
          expect(mockRedirect).toHaveBeenCalledWith('/404');
        });
      });
    });

    describe('409 (Conflict)', () => {
      it('should notify as a warning, not danger', async () => {
        await testErrorHandling('conflict', () => {
          testNotificationDisplayed('conflict error', 'warning');
        });
      });
    });

    describe('when request return a server error', () => {
      const requestError = [
        ['elastic_index_error', 'Failed to index documents: id1 '],
        [
          'validation_error',
          "validation failed:  .metadata['prop1'] should be string, .metadata['prop2'] is too long",
        ],
        ['unprocessable_entity', 'unprocessable entity'],
      ];

      it.each(requestError)('should notify the error with this message', async (url, message) => {
        const requestParams = new RequestParams({ key: 'test' }, { header: 'value' });
        try {
          await api.post(url, requestParams);
          fail('should throw error');
        } catch (e) {
          testNotificationDisplayed(message, 'danger');
        }
      });
    });

    describe('network error', () => {
      it('should notify that server is unreachable', async () => {
        await testErrorHandling('network_error', () => {
          testNotificationDisplayed('Could not reach server. Please try again later.');
        });
      });
    });

    describe('unknown error', () => {
      it('should show generic error message', async () => {
        await testErrorHandling('unknown_error', () => {
          testNotificationDisplayed('An error occurred');
        });
      });
    });

    it('should notify the user', async () => {
      await testErrorHandling('error_url', () => {
        testNotificationDisplayed(
          'An error has occurred, it has been logged with request id #1234.'
        );
      });
    });

    it('should end the loading bar', async () => {
      await testErrorHandling('error_url', () => {
        expect(loadingBar.done).toHaveBeenCalled();
      });
    });
  });
});