huridocs/uwazi

View on GitHub
app/api/i18n/specs/routes.spec.ts

Summary

Maintainability
A
0 mins
Test Coverage
import 'isomorphic-fetch';
import request from 'supertest';
import waitForExpect from 'wait-for-expect';

import * as csvApi from 'api/csv/csvLoader';
import { TranslationDBO } from 'api/i18n.v2/schemas/TranslationDBO';
import i18nRoutes from 'api/i18n/routes';
import settings from 'api/settings';
import { getFixturesFactory } from 'api/utils/fixturesFactory';
import { testingEnvironment } from 'api/utils/testingEnvironment';
import { TestEmitSources, iosocket, setUpApp } from 'api/utils/testingRoutes';
import { availableLanguages } from 'shared/languagesList';
import { LanguageSchema } from 'shared/types/commonTypes';
import { UserRole } from 'shared/types/userSchema';
import { DefaultTranslations } from '../defaultTranslations';
import { sortByLocale } from './sortByLocale';

describe('i18n translations routes', () => {
  const createTranslationDBO = getFixturesFactory().v2.database.translationDBO;
  const app = setUpApp(i18nRoutes, (req, _res, next) => {
    req.user = {
      username: 'admin',
      role: UserRole.ADMIN,
      email: 'admin@test.com',
    };
    // @ts-ignore
    req.file = { path: 'filder/filename.ext' };
    next();
  });

  beforeEach(async () => {
    const translationsV2: TranslationDBO[] = [
      createTranslationDBO('title', 'Plantilla 1', 'es', {
        id: 'contextID',
        type: 'Entity',
        label: 'Template',
      }),
      createTranslationDBO('Search', 'Buscar', 'es', {
        id: 'System',
        type: 'Entity',
        label: 'User Interface',
      }),
      createTranslationDBO('title', 'Template 1', 'en', {
        id: 'contextID',
        type: 'Entity',
        label: 'Template',
      }),
      createTranslationDBO('Search', 'Search', 'en', {
        id: 'System',
        type: 'Uwazi UI',
        label: 'User Interface',
      }),
    ];
    await testingEnvironment.setUp(
      {
        settings: [
          {
            languages: [
              { key: 'en', label: 'English', default: true },
              { key: 'es', label: 'Spanish', default: false },
            ],
          },
        ],
        translationsV2,
      },
      'index_i18n_routes'
    );
  });

  afterEach(() => {
    iosocket.emit.mockReset();
  });

  afterAll(async () => {
    await testingEnvironment.tearDown();
  });

  describe('GET', () => {
    describe('api/translations', () => {
      it('should return the translations', async () => {
        const response = await request(app).get('/api/translations').expect(200);

        expect(response.body.rows.sort(sortByLocale)).toMatchObject([
          {
            contexts: [
              {
                id: 'contextID',
                label: 'Template',
                type: 'Entity',
                values: { title: 'Template 1' },
              },
              {
                id: 'System',
                label: 'User Interface',
                type: 'Uwazi UI',
                values: {
                  Search: 'Search',
                },
              },
            ],
            locale: 'en',
          },

          {
            contexts: [
              {
                id: 'contextID',
                label: 'Template',
                type: 'Entity',
                values: { title: 'Plantilla 1' },
              },
              {
                id: 'System',
                label: 'User Interface',
                type: 'Uwazi UI',
                values: {
                  Search: 'Buscar',
                },
              },
            ],
            locale: 'es',
          },
        ]);
      });

      it('should only return the requested context', async () => {
        const appWithQuery = setUpApp(i18nRoutes, (req, _res, next) => {
          req.user = {
            username: 'admin',
            role: UserRole.ADMIN,
            email: 'admin@test.com',
          };
          req.query = { context: 'contextID' };
          next();
        });

        const response = await request(appWithQuery).get('/api/translations').expect(200);

        expect(response.body.rows.sort(sortByLocale)).toMatchObject([
          {
            contexts: [
              {
                id: 'contextID',
                label: 'Template',
                type: 'Entity',
                values: { title: 'Template 1' },
              },
            ],
            locale: 'en',
          },

          {
            contexts: [
              {
                id: 'contextID',
                label: 'Template',
                type: 'Entity',
                values: { title: 'Plantilla 1' },
              },
            ],
            locale: 'es',
          },
        ]);
      });
    });

    describe('api/languages', () => {
      it('should return the available languages', async () => {
        DefaultTranslations.CONTENTS_DIRECTORY = `${__dirname}/test_contents/1`;

        const response = await request(app).get('/api/languages').expect(200);

        const languagesWithPredefinedTranslations = response.body.filter(
          (language: LanguageSchema) => language.translationAvailable
        );

        expect(languagesWithPredefinedTranslations).toMatchObject([
          { key: 'ar' },
          { key: 'fr' },
          { key: 'es' },
        ]);
      });

      describe('when github returns any error', () => {
        it('should return an unaltered version of the languages list', async () => {
          DefaultTranslations.CONTENTS_DIRECTORY = `${__dirname}/non_valid`;

          const responseLanguages = await request(app).get('/api/languages').expect(200);

          expect(responseLanguages.body).toEqual(availableLanguages);
        });
      });
    });
  });

  describe('POST', () => {
    describe('api/translations', () => {
      it('should save the translation', async () => {
        const response = await request(app)
          .post('/api/translations')
          .send({
            locale: 'es',
            contexts: [
              {
                id: 'System',
                label: 'User Interface',
                type: 'Uwazi UI',
                values: { Search: 'Buscar' },
              },
            ],
          });

        expect(
          response.body.contexts.find((context: any) => context.id === 'System')
        ).toMatchObject({
          values: { Search: 'Buscar' },
        });

        expect(iosocket.emit).toHaveBeenCalledWith(
          'translationsChange',
          TestEmitSources.currentTenant,
          expect.objectContaining({
            contexts: expect.arrayContaining([
              expect.objectContaining({ values: { Search: 'Buscar' } }),
            ]),
            locale: 'es',
          })
        );
      });
    });

    describe('api/translations/languages', () => {
      describe('when successful', () => {
        let response: request.Response;
        let mockCalls: any[];

        const newSettings = {
          _id: expect.anything(),
          languages: [
            {
              _id: expect.anything(),
              key: 'en',
              label: 'English',
              default: true,
            },
            {
              _id: expect.anything(),
              key: 'es',
              label: 'Spanish',
              default: false,
            },
            {
              _id: expect.anything(),
              key: 'zh',
              label: 'Chinese',
            },
            {
              _id: expect.anything(),
              key: 'ja',
              label: 'Japanese',
            },
          ],
          mapStartingPoint: [
            {
              lon: 6,
              lat: 46,
            },
          ],
          links: [],
          filters: [],
        };

        beforeAll(async () => {
          DefaultTranslations.CONTENTS_DIRECTORY = `${__dirname}/test_contents/3`;

          response = await request(app)
            .post('/api/translations/languages')
            .send([
              { key: 'zh', label: 'Chinese' },
              { key: 'ja', label: 'Japanese' },
            ]);
          mockCalls = iosocket.emit.mock.calls;
          await waitForExpect(() => {
            expect(mockCalls.length).toBe(4);
          });
        });

        it('should return a 204', async () => {
          expect(response.status).toBe(204);
        });

        it('should emit a translationsChange event for each new language', async () => {
          const translationChangeEvents = mockCalls.filter(
            ([eventName]) => eventName === 'translationsChange'
          );
          expect(translationChangeEvents).toMatchObject([
            [
              'translationsChange',
              TestEmitSources.currentTenant,
              {
                locale: 'zh',
                contexts: [
                  {
                    id: 'System',
                    label: 'User Interface',
                    type: 'Uwazi UI',
                    values: {
                      Search: 'Search',
                    },
                  },
                  {
                    id: 'contextID',
                    label: 'Template',
                    type: 'Entity',
                    values: { title: 'Template 1' },
                  },
                ],
              },
            ],
            [
              'translationsChange',
              TestEmitSources.currentTenant,
              {
                locale: 'ja',
                contexts: [
                  {
                    id: 'System',
                    label: 'User Interface',
                    type: 'Uwazi UI',
                    values: { Search: 'Search' },
                  },
                  {
                    id: 'contextID',
                    label: 'Template',
                    type: 'Entity',
                    values: { title: 'Template 1' },
                  },
                ],
              },
            ],
          ]);
        });

        it('should emit an updateSettings event', async () => {
          const eventCandidate = mockCalls[mockCalls.length - 2];
          expect(eventCandidate).toMatchObject([
            'updateSettings',
            TestEmitSources.currentTenant,
            newSettings,
          ]);
        });

        it('should emit a translationsInstallDone event', async () => {
          const eventCandidate = mockCalls[mockCalls.length - 1];
          expect(eventCandidate).toMatchObject([
            'translationsInstallDone',
            TestEmitSources.session,
          ]);
        });
      });

      describe('when encountering an error', () => {
        let mockCalls: any[];
        jest.spyOn(console, 'error').mockImplementation(() => true);
        let response: request.Response;
        let settingsAddLanguageMock: jest.SpyInstance;

        beforeAll(async () => {
          DefaultTranslations.CONTENTS_DIRECTORY = `${__dirname}/test_contents/3`;

          settingsAddLanguageMock = jest.spyOn(settings, 'addLanguage');
          settingsAddLanguageMock.mockImplementation(() => {
            throw new Error('error message');
          });

          response = await request(app)
            .post('/api/translations/languages')
            .send([{ key: 'ja', label: 'Japanese' }]);
          mockCalls = iosocket.emit.mock.calls;
          await waitForExpect(() => {
            expect(mockCalls.length).toBe(1);
          });
        });

        afterAll(async () => {
          settingsAddLanguageMock.mockRestore();
        });

        it('should still return a 204', async () => {
          expect(response.status).toBe(204);
        });

        it('should emit a translationsInstallError event', async () => {
          const eventCandidate = mockCalls[0];
          expect(eventCandidate).toMatchObject([
            'translationsInstallError',
            TestEmitSources.session,
            'error message',
          ]);
        });
      });
    });

    describe('api/translations/populate', () => {
      it('should save the translations', async () => {
        DefaultTranslations.CONTENTS_DIRECTORY = `${__dirname}/test_contents/2`;

        const response = await request(app)
          .post('/api/translations/populate')
          .send({ locale: 'es' })
          .expect(200);

        expect(response.body).toMatchObject([
          {
            contexts: [
              {
                id: 'System',
                label: 'User Interface',
                type: 'Uwazi UI',
                values: {
                  Search: 'Buscar traducida',
                },
              },
              {
                id: 'contextID',
                label: 'Template',
                type: 'Entity',
                values: { title: 'Plantilla 1' },
              },
            ],
            locale: 'es',
          },
        ]);
      });
    });

    describe('api/translations/setasdeafult', () => {
      it('should update the setting', async () => {
        const response = await request(app)
          .post('/api/translations/setasdeafult')
          .send({ key: 'es' });

        expect(response.body).toMatchObject({
          languages: [
            {
              key: 'en',
              default: false,
            },
            {
              key: 'es',
              default: true,
            },
          ],
        });
        expect(iosocket.emit).toHaveBeenCalledWith(
          'updateSettings',
          TestEmitSources.currentTenant,
          expect.objectContaining({
            languages: [
              expect.objectContaining({ default: false, key: 'en', label: 'English' }),
              expect.objectContaining({ default: true, key: 'es', label: 'Spanish' }),
            ],
          })
        );
      });
    });

    describe('api/<translations>/import', () => {
      let csvLoaderMock: jest.SpyInstance;
      let loadTranslationsMock: jest.Mock;

      beforeEach(async () => {
        csvLoaderMock = jest.spyOn(csvApi, 'CSVLoader');
        csvLoaderMock.mockImplementation(() => {
          const mockObj = {
            loadTranslations: jest.fn(() => []),
          };
          loadTranslationsMock = mockObj.loadTranslations;
          return mockObj;
        });
      });

      afterAll(async () => {
        csvLoaderMock.mockRestore();
      });

      it.each([
        {
          body: { context: 0 },
          expectedError: 'type',
          expectedPath: '/body/context',
        },
        {
          req: { body: {}, file: { path: 'filepath' } },
          expectedError: 'required',
          expectedPath: '/body',
        },
      ])(
        'should return a validation error on $expectedError error',
        async ({ body, expectedError, expectedPath }) => {
          const response = await request(app)
            .post('/api/translations/import')
            .send(body)
            .expect(400);
          expect(response.body.errors[0].keyword).toBe(expectedError);
          expect(response.body.errors[0].instancePath).toBe(expectedPath);
          expect(response.body.error).toBe('validation failed');
        }
      );

      it('should load csv', async () => {
        await request(app)
          .post('/api/translations/import')
          .send({ context: 'context' })
          .expect(200);
        expect(loadTranslationsMock).toHaveBeenCalledWith('filder/filename.ext', 'context');
      });
    });
  });

  describe('DELETE', () => {
    describe('api/translations/languages', () => {
      describe('when successful', () => {
        let response: request.Response;
        let mockCalls: any[];

        beforeAll(async () => {
          response = await request(app).delete('/api/translations/languages?key=es').send();
          mockCalls = iosocket.emit.mock.calls;
          await waitForExpect(() => {
            expect(mockCalls.length).toBe(3);
          });
        });
        it('should return a 204', async () => {
          expect(response.status).toBe(204);
        });

        it('should emit an updateSettings event', async () => {
          const firstEvent = mockCalls[0];
          expect(firstEvent).toMatchObject([
            'updateSettings',
            TestEmitSources.currentTenant,
            {
              _id: expect.anything(),
              filters: [],
              languages: [
                {
                  _id: expect.anything(),
                  default: true,
                  key: 'en',
                  label: 'English',
                },
              ],
              links: [],
              mapStartingPoint: [
                {
                  lat: 46,
                  lon: 6,
                },
              ],
            },
          ]);
        });

        it('should emit a translationsDelete event', async () => {
          const eventCandidate = mockCalls[1];
          expect(eventCandidate).toMatchObject([
            'translationsDelete',
            TestEmitSources.currentTenant,
            'es',
          ]);
        });

        it('should emit a translationsDeleteDone event', async () => {
          const eventCandidate = mockCalls[2];
          expect(eventCandidate).toMatchObject(['translationsDeleteDone', TestEmitSources.session]);
        });
      });

      describe('when encountering an error', () => {
        let mockCalls: any[];
        let response: request.Response;
        let settingsDeleteLanguageMock: jest.SpyInstance;

        beforeAll(async () => {
          DefaultTranslations.CONTENTS_DIRECTORY = `${__dirname}/test_contents/3`;

          settingsDeleteLanguageMock = jest.spyOn(settings, 'deleteLanguage');
          settingsDeleteLanguageMock.mockImplementation(() => {
            throw new Error('error message');
          });

          response = await request(app).delete('/api/translations/languages?key=es').send();
          mockCalls = iosocket.emit.mock.calls;
          await waitForExpect(() => {
            expect(mockCalls.length).toBe(1);
          });
        });

        afterAll(async () => {
          settingsDeleteLanguageMock.mockRestore();
        });

        it('should still return a 204', async () => {
          expect(response.status).toBe(204);
        });

        it('should emit a translationsDeleteError event', async () => {
          const eventCandidate = mockCalls[0];
          expect(eventCandidate).toMatchObject([
            'translationsDeleteError',
            TestEmitSources.session,
            'error message',
          ]);
        });
      });
    });
  });
});