huridocs/uwazi

View on GitHub
app/api/entities/specs/entities.spec.js

Summary

Maintainability
A
0 mins
Test Coverage
/* eslint-disable max-lines */
/* eslint-disable max-nested-callbacks,max-statements */

import Ajv from 'ajv';
// eslint-disable-next-line node/no-restricted-import
import fs from 'fs/promises';

import entitiesModel from 'api/entities/entitiesModel';
import { spyOnEmit } from 'api/eventsbus/eventTesting';
import { uploadsPath, storage } from 'api/files';
import relationships from 'api/relationships';
import { search } from 'api/search';
import date from 'api/utils/date.js';
import db from 'api/utils/testing_db';
import { UserInContextMockFactory } from 'api/utils/testingUserInContext';
import { UserRole } from 'shared/types/userSchema';

import { applicationEventsBus } from 'api/eventsbus';
import fixtures, {
  adminId,
  batmanFinishesId,
  templateId,
  templateChangingNames,
  templateChangingNamesProps,
  syncPropertiesEntityId,
  templateWithEntityAsThesauri,
  docId1,
  uploadId1,
  uploadId2,
  unpublishedDocId,
  entityGetTestTemplateId,
} from './fixtures.js';
import entities from '../entities.js';
import { EntityUpdatedEvent } from '../events/EntityUpdatedEvent';
import { EntityDeletedEvent } from '../events/EntityDeletedEvent';
import { EntityCreatedEvent } from '../events/EntityCreatedEvent';

describe('entities', () => {
  const userFactory = new UserInContextMockFactory();

  beforeEach(async () => {
    jest.spyOn(search, 'delete').mockImplementation(async () => Promise.resolve());
    jest.spyOn(search, 'indexEntities').mockImplementation(async () => Promise.resolve());
    jest.spyOn(search, 'bulkIndex').mockImplementation(async () => Promise.resolve());
    jest.spyOn(search, 'bulkDelete').mockImplementation(async () => Promise.resolve());
    await db.setupFixturesAndContext(fixtures);
  });

  afterAll(async () => {
    await db.disconnect();
  });

  describe('save', () => {
    const saveDoc = async (doc, user) => {
      await entities.save(doc, { user, language: 'es' });
      const docs = await entities.get({ title: doc.title });
      return {
        createdDocumentEs: docs.find(d => d.language === 'es'),
        createdDocumentEn: docs.find(d => d.language === 'en'),
      };
    };

    it('should uniq the values on multiselect and relationship fields', async () => {
      const entity = {
        title: 'Batman begins',
        template: templateId,
        language: 'es',
        metadata: {
          multiselect: [
            { value: 'country_one' },
            { value: 'country_one' },
            { value: 'country_two' },
            { value: 'country_two' },
            { value: 'country_two' },
          ],
          friends: [
            { value: 'id1' },
            { value: 'id2' },
            { value: 'id2' },
            { value: 'id1' },
            { value: 'id3' },
            { value: 'id3' },
          ],
        },
      };
      const user = {};

      const createdEntity = await entities.save(entity, { user, language: 'es' });

      expect(createdEntity.metadata.multiselect.sort((a, b) => b.value < a.value)).toEqual([
        { value: 'country_one', label: 'Pais1' },
        { value: 'country_two', label: 'Pais2' },
      ]);
      expect(createdEntity.metadata.friends.sort((a, b) => b.value < a.value)).toEqual([
        { value: 'id1', label: 'entity one', type: 'entity' },
        { value: 'id2', label: 'entity two', type: 'entity' },
        { value: 'id3', label: 'entity three', type: 'entity' },
      ]);
    });

    it('should create a new entity for each language in settings with a language property, a shared id, and default template', async () => {
      const universalTime = 1;
      jest.spyOn(date, 'currentUTC').mockImplementation(() => universalTime);
      const doc = { title: 'Batman begins' };
      const user = { _id: db.id() };

      const { createdDocumentEs, createdDocumentEn } = await saveDoc(doc, user);

      expect(createdDocumentEs.sharedId).toBe(createdDocumentEn.sharedId);

      expect(createdDocumentEs.template.toString()).toBe(templateChangingNames.toString());
      expect(createdDocumentEn.template.toString()).toBe(templateChangingNames.toString());

      expect(createdDocumentEs.title).toBe(doc.title);
      expect(createdDocumentEs.user.equals(user._id)).toBe(true);
      expect(createdDocumentEs.published).toBe(false);
      expect(createdDocumentEs.creationDate).toEqual(universalTime);
      expect(createdDocumentEs.editDate).toEqual(universalTime);

      expect(createdDocumentEn.title).toBe(doc.title);
      expect(createdDocumentEn.user.equals(user._id)).toBe(true);
      expect(createdDocumentEn.published).toBe(false);
      expect(createdDocumentEn.creationDate).toEqual(universalTime);
    });

    it('should create a new entity for each language when passing an _id', async () => {
      const universalTime = 1;
      jest.spyOn(date, 'currentUTC').mockImplementation(() => universalTime);
      const doc = { _id: unpublishedDocId, title: 'Batman begins', language: 'es' };
      const user = { _id: db.id() };

      const { createdDocumentEs, createdDocumentEn } = await saveDoc(doc, user);

      expect(createdDocumentEs._id.toString()).toBe(unpublishedDocId.toString());
      expect(createdDocumentEn._id.toString()).not.toBe(unpublishedDocId.toString());
    });

    it('should create a new entity, preserving template if passed', async () => {
      const doc = { title: 'The Dark Knight', template: templateId };
      const user = { _id: db.id() };
      const { createdDocumentEs, createdDocumentEn } = await saveDoc(doc, user);

      expect(createdDocumentEs.template.toString()).toBe(templateId.toString());
      expect(createdDocumentEn.template.toString()).toBe(templateId.toString());
    });

    it('should set default template and default metadata', async () => {
      const doc = {
        title: 'the dark knight',
        fullText: { 0: 'the full text!' },
      };
      const user = { _id: db.id() };

      const createdDocument = await entities.save(doc, { user, language: 'en' });

      expect(createdDocument._id).toBeDefined();
      expect(createdDocument.title).toBe(doc.title);
      expect(createdDocument.user.equals(user._id)).toBe(true);
      expect(createdDocument.language).toEqual('en');
      expect(createdDocument.fullText).not.toBeDefined();
      expect(createdDocument.metadata).toEqual({
        property1: [],
        property2: [],
        property3: [],
      });
      expect(createdDocument.template).toBeDefined();
    });

    it('should return updated entity with updated editDate', async () => {
      const updateTime = 2;
      const doc = {
        title: 'the dark knight',
        fullText: { 0: 'the full text!' },
      };

      const user = { _id: db.id() };

      const createdDocument = await entities.save(doc, { user, language: 'en' });
      jest.spyOn(date, 'currentUTC').mockImplementation(() => updateTime);
      const updatedDocument = await entities.save(
        { ...createdDocument, title: 'updated title' },
        { user, language: 'en' }
      );
      expect(updatedDocument.title).toBe('updated title');
      expect(updatedDocument.editDate).toEqual(updateTime);
    });

    it('should index the newly created documents', async () => {
      const doc = { title: 'the dark knight', template: templateId };
      const user = { _id: db.id() };

      await entities.save(doc, { user, language: 'en' });
      expect(search.indexEntities).toHaveBeenCalled();
    });

    it('should allow partial saves with correct full indexing (NOTE!: partial update requires sending sharedId)', async () => {
      const partialDoc = {
        _id: batmanFinishesId,
        sharedId: 'shared',
        title: 'Updated title',
        language: 'en',
      };
      const savedEntity = await entities.save(partialDoc, { language: 'en' });
      expect(savedEntity.title).toBe('Updated title');
      expect(savedEntity.metadata.property1).toEqual([{ value: 'value1' }]);
      expect(savedEntity.metadata.friends).toEqual([
        { icon: null, label: 'shared2title', type: 'entity', value: 'shared2' },
      ]);
      const refetchedEntity = await entities.getById(batmanFinishesId);
      expect(refetchedEntity.title).toBe('Updated title');
      expect(refetchedEntity.metadata.property1).toEqual([{ value: 'value1' }]);
      expect(refetchedEntity.metadata.friends).toEqual([
        { icon: null, label: 'shared2title', type: 'entity', value: 'shared2' },
      ]);
      expect(search.indexEntities).toHaveBeenCalled();
    });

    describe('when other languages have no metadata', () => {
      it('should replicate metadata being saved', async () => {
        const doc = {
          _id: batmanFinishesId,
          sharedId: 'shared',
          metadata: { text: [{ value: 'newMetadata' }] },
          template: templateId,
        };

        const updatedDoc = await entities.save(doc, { language: 'en' });
        expect(updatedDoc.language).toBe('en');
        const [docES, docEN, docPT] = await Promise.all([
          entities.getById('shared', 'es'),
          entities.getById('shared', 'en'),
          entities.getById('shared', 'pt'),
        ]);
        expect(docEN.published).toBe(true);
        expect(docES.published).toBe(true);
        expect(docPT.published).toBe(true);

        expect(docEN.metadata.text).toEqual([{ value: 'newMetadata' }]);
        expect(docES.metadata.text).toEqual([{ value: 'newMetadata' }]);
        expect(docPT.metadata.text).toEqual([{ value: 'test' }]);
      });
    });

    describe('when published/template/generatedToc property changes', () => {
      it('should replicate the change for all the languages and ignore the published field', async () => {
        const doc = {
          _id: batmanFinishesId,
          sharedId: 'shared',
          metadata: {},
          published: false,
          template: templateId,
          generatedToc: true,
        };

        const updatedDoc = await entities.save(doc, { language: 'en' });
        expect(updatedDoc.language).toBe('en');
        const [docES, docEN] = await Promise.all([
          entities.getById('shared', 'es'),
          entities.getById('shared', 'en'),
        ]);
        expect(docEN.template).toBeDefined();
        expect(docES.template).toBeDefined();

        expect(docES.published).toBe(true);
        expect(docES.generatedToc).toBe(true);
        expect(docES.template.equals(templateId)).toBe(true);
        expect(docEN.published).toBe(true);
        expect(docEN.generatedToc).toBe(true);
        expect(docEN.template.equals(templateId)).toBe(true);
      });
    });

    it('should ignore the permissions parameter', async () => {
      const doc = {
        _id: unpublishedDocId,
        sharedId: 'other',
        metadata: {},
        permissions: [],
      };

      const updatedDoc = await entities.save(doc, { language: 'en' });

      expect(updatedDoc.permissions).toEqual([
        expect.objectContaining({ refId: 'user1' }),
        expect.objectContaining({ refId: 'user2' }),
      ]);
    });

    describe('when generatedToc is undefined', () => {
      it('should not replicate the value to all languages', async () => {
        const doc = { _id: batmanFinishesId, sharedId: 'shared', generatedToc: true };
        await entities.save(doc, { language: 'en' });
        await entities.save({ _id: batmanFinishesId, sharedId: 'shared' }, { language: 'en' });
        const [docES, docEN] = await Promise.all([
          entities.getById('shared', 'es'),
          entities.getById('shared', 'en'),
        ]);

        expect(docES.generatedToc).toBe(true);
        expect(docEN.generatedToc).toBe(true);
      });
    });

    it('should sync select/multiselect/dates/multidate/multidaterange/numeric', async () => {
      const doc = {
        _id: syncPropertiesEntityId,
        sharedId: 'shared1',
        template: templateId,
        language: 'en',
        metadata: {
          text: [{ value: 'changedText' }],
          select: [{ value: 'country_one' }],
          multiselect: [{ value: 'country_two' }],
          date: [{ value: 1234 }],
          multidate: [{ value: 1234 }],
          multidaterange: [{ value: { from: 1, to: 2 } }],
          numeric: [{ value: 100 }],
        },
      };

      const updatedDoc = await entities.save(doc, { language: 'en' });
      expect(updatedDoc.language).toBe('en');
      const [docEN, docES, docPT] = await Promise.all([
        entities.getById('shared1', 'en'),
        entities.getById('shared1', 'es'),
        entities.getById('shared1', 'pt'),
      ]);
      expect(docEN.metadata.text[0].value).toBe('changedText');
      expect(docEN.metadata.select[0]).toEqual({ value: 'country_one', label: 'Country1' });
      expect(docEN.metadata.multiselect).toEqual([
        {
          value: 'country_two',
          label: 'Country2',
        },
      ]);
      expect(docEN.metadata.date[0].value).toBe(1234);
      expect(docEN.metadata.multidate).toEqual([{ value: 1234 }]);
      expect(docEN.metadata.multidaterange).toEqual([{ value: { from: 1, to: 2 } }]);
      expect(docEN.metadata.numeric[0].value).toEqual(100);

      expect(docES.metadata.property1[0].value).toBe('text');
      expect(docES.metadata.select[0]).toEqual({ value: 'country_one', label: 'Pais1' });
      expect(docES.metadata.multiselect).toEqual([
        {
          value: 'country_two',
          label: 'Pais2',
        },
      ]);
      expect(docES.metadata.date[0].value).toBe(1234);
      expect(docES.metadata.multidate).toEqual([{ value: 1234 }]);
      expect(docES.metadata.multidaterange).toEqual([{ value: { from: 1, to: 2 } }]);
      expect(docES.metadata.numeric[0].value).toEqual(100);

      expect(docPT.metadata.property1[0].value).toBe('text');
      expect(docPT.metadata.select[0]).toEqual({ value: 'country_one', label: 'Pais1_pt' });
      expect(docPT.metadata.multiselect).toEqual([
        {
          value: 'country_two',
          label: 'Pais2_pt',
        },
      ]);
      expect(docPT.metadata.date[0].value).toBe(1234);
      expect(docPT.metadata.multidate).toEqual([{ value: 1234 }]);
      expect(docPT.metadata.multidaterange).toEqual([{ value: { from: 1, to: 2 } }]);
      expect(docPT.metadata.numeric[0].value).toEqual(100);
    });

    describe('saveEntityBasedReferences', () => {
      it('should save references on creation', async () => {
        jest.spyOn(date, 'currentUTC').mockReturnValue(1);
        const entity = {
          title: 'Batman begins',
          template: templateId,
          language: 'es',
          metadata: {
            friends: [{ value: 'id1' }, { value: 'id2' }, { value: 'id3' }],
            enemies: [{ value: 'shared1' }],
          },
        };
        const user = { _id: db.id() };

        const createdEntity = await entities.save(entity, { user, language: 'es' });

        const createdRelationships = await relationships.getByDocument(
          createdEntity.sharedId,
          'es'
        );

        expect(createdRelationships.length).toBe(6);
        expect(createdRelationships.map(r => r.entityData.title).sort()).toEqual([
          'Batman begins',
          'Batman begins',
          'ES',
          'entity one',
          'entity three',
          'entity two',
        ]);
      });

      it('should add references on update', async () => {
        const user = { _id: adminId };

        const existing = await entities.getById('relSaveTest', 'en');
        const existingRelationships = await relationships.getByDocument('relSaveTest', 'en');
        expect(existingRelationships.length).toBe(4);
        expect(existingRelationships.map(r => r.entityData.title).sort()).toEqual([
          'Batman still not done',
          'Batman still not done',
          'shared2title',
          'shared2title',
        ]);

        existing.metadata.friends.push({ value: 'id1' }, { value: 'id2' });
        existing.metadata.enemies.push({ value: 'shared1' });
        await entities.save(existing, { user, language: 'en' });

        const updatedRelationships = await relationships.getByDocument('relSaveTest', 'en');
        expect(updatedRelationships.map(r => r.entityData.title).sort()).toEqual([
          'Batman still not done',
          'Batman still not done',
          'EN',
          'entity one',
          'entity two',
          'shared2title',
          'shared2title',
        ]);
        expect(updatedRelationships.length).toBe(7);
      });

      it('should delete references on update', async () => {
        const user = { _id: adminId };

        const existing = await entities.getById('relSaveTest', 'en');
        const existingRelationships = await relationships.getByDocument('relSaveTest', 'en');
        expect(existingRelationships.length).toBe(4);
        expect(existingRelationships.map(r => r.entityData.title).sort()).toEqual([
          'Batman still not done',
          'Batman still not done',
          'shared2title',
          'shared2title',
        ]);

        existing.metadata.friends = [];
        existing.metadata.enemies = [];
        await entities.save(existing, { user, language: 'en' });

        const updatedRelationships = await relationships.getByDocument('relSaveTest', 'en');
        expect(updatedRelationships.length).toBe(0);
      });
    });

    it('should not circle back to updateMetdataFromRelationships', async () => {
      jest.spyOn(date, 'currentUTC').mockReturnValue(1);
      jest.spyOn(entities, 'updateMetdataFromRelationships');
      const doc = {
        _id: batmanFinishesId,
        sharedId: 'shared',
        type: 'entity',
        template: templateId,
        language: 'en',
        title: 'Batman finishes',
        published: true,
        fullText: {
          1: 'page[[1]] 1[[1]]',
          2: 'page[[2]] 2[[2]]',
          3: '',
        },
        metadata: {
          property1: [{ value: 'value1' }],
          friends: [],
        },
        file: {
          filename: '8202c463d6158af8065022d9b5014cc1.pdf',
        },
      };
      const user = { _id: db.id() };

      await entities.save(doc, { user, language: 'es' }, false);
      expect(entities.updateMetdataFromRelationships).not.toHaveBeenCalled();
    });

    describe('when document have _id', () => {
      it('should not assign again user and creation date', async () => {
        jest.spyOn(date, 'currentUTC').mockReturnValue(10);
        const modifiedDoc = { _id: batmanFinishesId, sharedId: 'shared' };
        await entities.save(modifiedDoc, { user: 'another_user', language: 'en' });
        const doc = await entities.getById('shared', 'en');
        expect(doc.user).not.toBe('another_user');
        expect(doc.creationDate).not.toBe(10);
      });

      it('should return the previously saved documents of the entity', async () => {
        const modifiedDoc = { _id: batmanFinishesId, sharedId: 'shared' };
        const doc = await entities.save(modifiedDoc, {
          language: 'en',
        });
        expect(doc.documents[0].entity).toBe('shared');
      });
    });

    describe('save entity without a logged user', () => {
      it('should save the entity with unrestricted access', async () => {
        const user = {};
        userFactory.mock(undefined);
        const entity = { title: 'Batman begins', template: templateId, language: 'es' };
        const createdEntity = await entities.save(entity, { user, language: 'es' });
        expect(createdEntity._id).not.toBeUndefined();
        expect(createdEntity.title).toEqual(entity.title);
        userFactory.mockEditorUser();
      });
    });

    describe('events', () => {
      let emitSpy;

      beforeEach(() => {
        emitSpy = jest.spyOn(applicationEventsBus, 'emit');
        emitSpy.mockClear();
      });

      it('should emit an event when an entity is created', async () => {
        const newEntity = {
          template: templateId,
          title: 'New Super Hero',
          metadata: {
            text: [{ value: 'New Text' }],
            property1: [{ value: 'value1' }],
            property2: [{ value: 'value2' }],
            description: [{ value: 'ew Description' }],
            friends: [{ icon: null, label: 'shared2title', type: 'entity', value: 'shared2' }],
            enemies: [{ icon: null, label: 'shared2title', type: 'entity', value: 'shared2' }],
            select: [],
          },
        };

        const savedEntity = await entities.save(newEntity, {
          user: { _id: adminId },
          language: 'en',
        });

        const afterAllLanguages = await entities.getAllLanguages(savedEntity.sharedId);

        expect(emitSpy.mock.calls[0][0]).toBeInstanceOf(EntityCreatedEvent);
        expect(emitSpy).toHaveBeenCalledWith(
          new EntityCreatedEvent({
            entities: afterAllLanguages,
            targetLanguageKey: 'en',
          })
        );
      });

      it('should emit an event when an entity is updated', async () => {
        const before = fixtures.entities.find(e => e._id === batmanFinishesId);
        const beforeAllLanguages = await entities.getAllLanguages(before.sharedId);
        const after = { ...before, title: 'new title' };

        await entities.save(after, { language: 'en' });

        const afterAllLanguages = await entities.getAllLanguages(before.sharedId);

        expect(emitSpy.mock.calls[0][0]).toBeInstanceOf(EntityUpdatedEvent);
        expect(emitSpy).toHaveBeenCalledWith(
          new EntityUpdatedEvent({
            before: beforeAllLanguages,
            after: afterAllLanguages,
            targetLanguageKey: 'en',
          })
        );
      });
    });
  });

  describe('updateMetdataFromRelationships', () => {
    it('should update the metdata based on the entity relationships', async () => {
      await entities.updateMetdataFromRelationships(['shared', 'missingEntity'], 'en');
      const updatedEntity = await entities.getById('shared', 'en');
      expect(updatedEntity.metadata.friends).toEqual([
        { icon: null, type: 'entity', label: 'shared2title', value: 'shared2' },
      ]);
    });

    it('should not fail on newly created documents (without metadata)', async () => {
      const doc = { title: 'Batman begins', template: templateId };
      const user = { _id: db.id() };
      const newEntity = await entities.save(doc, { user, language: 'es' });

      await entities.updateMetdataFromRelationships([newEntity.sharedId], 'es');
      const updatedEntity = await entities.getById(newEntity.sharedId, 'en');
      expect(updatedEntity.metadata).toEqual({
        date: [],
        daterange: [],
        description: [],
        enemies: [],
        field_nested: [],
        friends: [],
        multidate: [],
        multidaterange: [],
        multiselect: [],
        numeric: [],
        property1: [],
        property2: [],
        select: [],
        text: [],
      });
    });

    it('should sanitize the entities', async () => {
      const sanitizationSpy = jest.spyOn(entities, 'sanitize');
      await entities.updateMetdataFromRelationships(['shared'], 'en');

      expect(sanitizationSpy.mock.calls).toMatchObject([
        [
          {
            sharedId: 'shared',
            language: 'en',
            title: 'Batman finishes',
          },
          {
            name: 'template_test',
          },
        ],
      ]);
      sanitizationSpy.mockRestore();
    });

    describe('unrestricted for collaborator', () => {
      it('should save the entity with unrestricted access', async () => {
        userFactory.mock({
          _id: 'user1',
          role: UserRole.COLLABORATOR,
          username: 'User 1',
          email: 'col@test.com',
        });

        await entities.updateMetdataFromRelationships(['shared'], 'en');
        const updatedEntity = await entities.getById('shared', 'en');
        expect(updatedEntity.metadata.friends).toEqual([
          { icon: null, type: 'entity', label: 'shared2title', value: 'shared2' },
        ]);
        userFactory.mockEditorUser();
      });
    });
  });

  describe('Sanitize', () => {
    it('should sanitize multidates, removing non valid dates', async () => {
      const doc = {
        _id: batmanFinishesId,
        sharedId: 'shared',
        metadata: {
          multidate: [{ value: null }, { value: 1234 }, { value: null }, { value: 5678 }],
        },
        published: false,
        template: templateId,
      };

      const updatedDoc = await entities.save(doc, { language: 'en' });
      expect(updatedDoc.language).toBe('en');
      const [docES, docEN] = await Promise.all([
        entities.getById('shared', 'es'),
        entities.getById('shared', 'en'),
      ]);
      expect(docES.metadata.multidate).toEqual([{ value: 1234 }, { value: 5678 }]);
      expect(docEN.metadata.multidate).toEqual([{ value: 1234 }, { value: 5678 }]);
    });

    it('should sanitize select, removing empty values', async () => {
      const doc = {
        _id: batmanFinishesId,
        sharedId: 'shared',
        metadata: { select: [{ value: '' }] },
        published: false,
        template: templateId,
      };

      const updatedDoc = await entities.save(doc, { language: 'en' });
      expect(updatedDoc.language).toBe('en');
      const [docES, docEN] = await Promise.all([
        entities.getById('shared', 'es'),
        entities.getById('shared', 'en'),
      ]);
      expect(docES.metadata.select).toEqual([]);
      expect(docEN.metadata.select).toEqual([]);
    });

    it('should sanitize daterange, removing non valid dates', async () => {
      const doc1 = {
        _id: batmanFinishesId,
        sharedId: 'shared',
        metadata: { daterange: [{ value: { from: 1, to: 2 } }] },
        template: templateId,
      };
      const doc2 = {
        _id: batmanFinishesId,
        sharedId: 'shared',
        metadata: { daterange: [{ value: { from: null, to: 2 } }] },
        template: templateId,
      };
      const doc3 = {
        _id: batmanFinishesId,
        sharedId: 'shared',
        metadata: { daterange: [{ value: { from: 2, to: null } }] },
        template: templateId,
      };
      const doc4 = {
        _id: batmanFinishesId,
        sharedId: 'shared',
        metadata: { daterange: [{ value: { from: null, to: null } }] },
        template: templateId,
      };

      await entities.save(doc1, { language: 'en' });
      const doc = await entities.getById('shared', 'en');
      expect(doc.metadata.daterange).toEqual(doc1.metadata.daterange);
      await entities.save(doc2, { language: 'en' });
      const doc1db = await entities.getById('shared', 'en');
      expect(doc1db.metadata.daterange).toEqual(doc2.metadata.daterange);
      await entities.save(doc3, { language: 'en' });
      const doc2db = await entities.getById('shared', 'en');
      expect(doc2db.metadata.daterange).toEqual(doc3.metadata.daterange);
      await entities.save(doc4, { language: 'en' });
      const doc3db = await entities.getById('shared', 'en');
      expect(doc3db.metadata.daterange).toEqual([]);
    });

    it('should sanitize multidaterange, removing non valid dates', async () => {
      const doc = {
        _id: batmanFinishesId,
        sharedId: 'shared',
        metadata: {
          multidaterange: [
            { value: { from: 1, to: 2 } },
            { value: { from: null, to: null } },
            { value: { from: null, to: 2 } },
            { value: { from: 2, to: null } },
            { value: { from: null, to: null } },
          ],
        },
        published: false,
        template: templateId,
      };

      const updatedDoc = await entities.save(doc, { language: 'en' });
      expect(updatedDoc.language).toBe('en');
      const [docES, docEN] = await Promise.all([
        entities.getById('shared', 'es'),
        entities.getById('shared', 'en'),
      ]);
      expect(docES.metadata.multidaterange).toEqual([
        { value: { from: 1, to: 2 } },
        { value: { from: null, to: 2 } },
        { value: { from: 2, to: null } },
      ]);
      expect(docEN.metadata.multidaterange).toEqual([
        { value: { from: 1, to: 2 } },
        { value: { from: null, to: 2 } },
        { value: { from: 2, to: null } },
      ]);
    });

    it('should sanitize numeric, parsing texts into numbers', async () => {
      const doc1 = {
        _id: batmanFinishesId,
        sharedId: 'shared',
        metadata: { numeric: [{ value: '10' }] },
        template: templateId,
      };
      const doc2 = {
        _id: batmanFinishesId,
        sharedId: 'shared',
        metadata: { numeric: [{ value: '10.5' }] },
        template: templateId,
      };

      await entities.save(doc1, { language: 'en' });
      const doc = await entities.getById('shared', 'en');
      expect(doc.metadata.numeric).toEqual([{ value: 10 }]);
      await entities.save(doc2, { language: 'en' });
      const doc1db = await entities.getById('shared', 'en');
      expect(doc1db.metadata.numeric).toEqual([{ value: 10.5 }]);
    });

    it('should sanitize numeric, parsing empty strings into no value', async () => {
      const doc1 = {
        _id: batmanFinishesId,
        sharedId: 'shared',
        metadata: { numeric: [{ value: '' }] },
        template: templateId,
      };

      await entities.save(doc1, { language: 'en' });
      const doc = await entities.getById('shared', 'en');
      expect(doc.metadata.numeric).toEqual(undefined);
    });

    it('should supply empty arrays for missing metadata, for all languages', async () => {
      const user = { _id: db.id() };
      const doc1 = {
        title: 'newEntity',
        metadata: { text: [{ value: 'text' }], numeric: [{ value: 1 }] },
        template: templateId,
      };

      await entities.save(doc1, { user, language: 'en' });
      const docs = await entities.get({ title: 'newEntity' });
      expect(docs.length).toBe(3);
      expect(docs.map(d => d.language).sort()).toEqual(['en', 'es', 'pt']);
      docs.forEach(doc => {
        expect(doc.metadata).toEqual({
          text: [{ value: 'text' }],
          property1: [],
          property2: [],
          description: [],
          select: [],
          multiselect: [],
          date: [],
          multidate: [],
          multidaterange: [],
          daterange: [],
          friends: [],
          enemies: [],
          field_nested: [],
          numeric: [{ value: 1 }],
        });
      });
    });
  });

  describe('get', () => {
    const checkFilenames = (expectedFilenames, entity, property) => {
      if (expectedFilenames !== null) {
        expect(entity[property].length).toBe(expectedFilenames.length);
        entity[property].forEach((element, index) => {
          expect(element.filename).toBe(expectedFilenames[index]);
        });
      } else {
        expect(entity).not.toHaveProperty(property);
      }
    };

    const checkEntityGetResult = (entity, title, documentFilenames, attachmentFilenames) => {
      expect(entity.title).toBe(title);

      checkFilenames(documentFilenames, entity, 'documents');
      checkFilenames(attachmentFilenames, entity, 'attachments');
    };

    it('should return matching entities for the conditions', async () => {
      const sharedId = 'shared1';

      const [enDoc, esDoc] = await Promise.all([
        entities.get({ sharedId, language: 'en' }),
        entities.get({ sharedId, language: 'es' }),
      ]);
      expect(enDoc[0].title).toBe('EN');
      expect(esDoc[0].title).toBe('ES');
    });

    it('should return documents and attachments properly, when requested.', async () => {
      const result = await entities.get({ template: entityGetTestTemplateId });
      checkEntityGetResult(result[0], 'TitleA', ['file2.name'], []);
      checkEntityGetResult(result[1], 'TitleB', [], []);
      checkEntityGetResult(result[2], 'TitleC', ['file3.name'], ['file1.name', 'file4.name']);
    });

    it('should return documents and attachments properly while using a select clause in the query.', async () => {
      const result = await entities.get({ template: entityGetTestTemplateId }, { title: true });
      checkEntityGetResult(result[0], 'TitleA', ['file2.name'], []);
      checkEntityGetResult(result[1], 'TitleB', [], []);
      checkEntityGetResult(result[2], 'TitleC', ['file3.name'], ['file1.name', 'file4.name']);
    });

    it('should not return documents and attachments, when not requested.', async () => {
      const result = await entities.get(
        { template: entityGetTestTemplateId },
        {},
        { withoutDocuments: true }
      );
      checkEntityGetResult(result[0], 'TitleA', null, null);
      checkEntityGetResult(result[1], 'TitleB', null, null);
      checkEntityGetResult(result[2], 'TitleC', null, null);
    });

    it.each([
      [undefined, undefined],
      ['title', 'title sharedId'],
      ['+title', '+title +sharedId'],
      [['title'], ['title', 'sharedId']],
      [{}, {}],
      [{ title: 1 }, { title: 1, sharedId: 1 }],
    ])(
      'should call model.get with a properly extended select: %s -> %s',
      async (select, extended) => {
        const entitesModelGet = jest.spyOn(entitiesModel, 'get');
        await entities.get({ template: entityGetTestTemplateId }, select);
        expect(entitesModelGet).toBeCalledWith({ template: entityGetTestTemplateId }, extended, {});
        entitesModelGet.mockRestore();
      }
    );
  });

  describe('getWithRelationships', () => {
    it('should return the entities with its permitted relationships when no user', async () => {
      userFactory.mock(undefined);
      const [result] = await entities.getWithRelationships({ sharedId: 'getWithRelRoot' });
      expect(result.relations).toEqual([
        expect.objectContaining({ entity: 'getWithRelRoot' }),
        expect.objectContaining({ entity: 'getWithRelPublic' }),
      ]);
      userFactory.mockEditorUser();
    });

    it('should return the entities with its permitted relationships when the user has permissions', async () => {
      userFactory.mockEditorUser();
      const [result] = await entities.getWithRelationships({ sharedId: 'getWithRelRoot' });
      expect(result.relations).toEqual([
        expect.objectContaining({ entity: 'getWithRelRoot' }),
        expect.objectContaining({ entity: 'getWithRelPublic' }),
        expect.objectContaining({ entity: 'getWithRelPrivate' }),
      ]);
    });
  });

  describe('denormalize', () => {
    it('should denormalize entity with missing metadata labels', async () => {
      userFactory.mock({
        _id: 'user1',
        username: 'collaborator',
        role: UserRole.COLLABORATOR,
      });
      const entity = (await entities.get({ sharedId: 'shared', language: 'en' }))[0];
      entity.metadata.friends[0].label = '';
      const denormalized = await entities.denormalize(entity, { user: 'dummy', language: 'en' });
      expect(denormalized.metadata.friends[0].label).toBe('shared2title');
    });

    it('should denormalize inherited metadata', async () => {
      const entity = (await entities.get({ sharedId: 'shared', language: 'en' }))[0];

      const denormalized = await entities.denormalize(entity, { user: 'dummy', language: 'en' });
      expect(denormalized.metadata.enemies[0].inheritedValue).toEqual([
        { value: 'something to be inherited' },
      ]);
      expect(denormalized.metadata.enemies[0].inheritedType).toBe('text');
    });

    it('should denormalize thesauri categories as parents', async () => {
      const entity = {
        template: templateId,
        title: 'Thesauri categories test',
        language: 'en',
        metadata: {
          select: [{ value: 'town1' }],
          multiselect: [{ value: 'country_one' }, { value: 'town2' }],
        },
      };
      const denormalized = await entities.denormalize(entity, { user: 'dummy', language: 'en' });
      expect(denormalized.metadata.select[0].parent).toEqual({ value: 'towns', label: 'Towns' });
    });
  });

  describe('countByTemplate', () => {
    it('should return how many entities using the template passed', async () => {
      const count = await entities.countByTemplate(templateId);
      expect(count).toBe(10);
    });

    it('should return 0 when no count found', done => {
      entities
        .countByTemplate(db.id())
        .then(count => {
          expect(count).toBe(0);
          done();
        })
        .catch(done.fail);
    });
  });

  describe('getByTemplate', () => {
    it('should return only published entities with passed template and language', done => {
      entities
        .getByTemplate(templateId, 'en')
        .then(docs => {
          expect(docs.length).toBe(3);
          expect(docs[0].title).toBe('Batman finishes');
          expect(docs[1].title).toBe('Batman still not done');
          expect(docs[2].title).toBe('EN');
          done();
        })
        .catch(done.fail);
    });

    it('should return all entities (including unpublished) if required', async () => {
      const docs = await entities.getByTemplate(templateId, 'en', null, false);
      expect(docs.length).toBe(7);
      expect(docs.sort((a, b) => a.title.localeCompare(b.title)).map(d => d.title)).toEqual([
        'Batman finishes',
        'Batman still not done',
        'EN',
        'shared2title',
        'Unpublished entity',
        'value0',
        'value2',
      ]);
    });

    it('should return all entities (including unpublished) if required and user is a collaborator', async () => {
      userFactory.mock({
        _id: 'user1',
        role: 'collaborator',
        groups: [],
      });
      const docs = (await entities.getByTemplate(templateId, 'en', null, false)).sort((a, b) =>
        b.title.localeCompare(a.title)
      );
      expect(docs.length).toBe(4);
      expect(docs[0].title).toBe('Unpublished entity');
      expect(docs[1].title).toBe('EN');
      expect(docs[2].title).toBe('Batman still not done');
      expect(docs[3].title).toBe('Batman finishes');
    });
  });

  describe('multipleUpdate()', () => {
    it('should save() all the entities with the new metadata', async () => {
      const metadata = {
        property1: [{ value: 'new text' }],
        description: [{ value: 'yeah!' }],
        friends: [{ icon: null, label: 'shared2title', type: 'entity', value: 'shared2' }],
      };

      const updatedEntities = await entities.multipleUpdate(
        ['shared', 'shared1', 'non_existent'],
        { icon: { label: 'test' }, published: false, metadata },
        { language: 'en' }
      );

      expect(updatedEntities.length).toBe(2);

      const sharedEntity = updatedEntities.find(e => e.sharedId === 'shared');
      expect(sharedEntity).toEqual(
        expect.objectContaining({
          sharedId: 'shared',
          language: 'en',
          icon: { label: 'test' },
          published: true,
          metadata: expect.objectContaining(metadata),
        })
      );

      const shared1Entity = updatedEntities.find(e => e.sharedId === 'shared1');
      expect(shared1Entity).toEqual(
        expect.objectContaining({
          sharedId: 'shared1',
          language: 'en',
          icon: { label: 'test' },
          published: true,
          metadata: expect.objectContaining(metadata),
        })
      );
    });

    it('should save() all the entities with the diffMetadata', async () => {
      const updatedEntities1 = await entities.multipleUpdate(
        ['shared', 'other', 'non_existent'],
        {
          icon: { label: 'test' },
          published: false,
          diffMetadata: {
            multiselect: {
              added: [{ value: 'country_one' }],
              removed: [{ value: 'country_two' }],
            },
          },
        },
        { language: 'en' }
      );

      const updatedEntities2 = await entities.multipleUpdate(
        ['shared'],
        {
          diffMetadata: {
            multiselect: {
              added: [{ value: 'country_two' }],
              removed: [{ value: 'country_one' }],
            },
          },
        },
        { language: 'en' }
      );

      expect(updatedEntities1.length).toBe(2);
      expect(updatedEntities2.length).toBe(1);

      const sharedEntity = updatedEntities2.find(e => e.sharedId === 'shared');
      expect(sharedEntity).toEqual(
        expect.objectContaining({
          sharedId: 'shared',
          language: 'en',
          icon: { label: 'test' },
          published: true,
          metadata: expect.objectContaining({
            multiselect: [
              {
                label: 'Country2',
                value: 'country_two',
              },
            ],
          }),
        })
      );

      const shared1Entity = updatedEntities1.find(e => e.sharedId === 'other');
      expect(shared1Entity).toEqual(
        expect.objectContaining({
          sharedId: 'other',
          language: 'en',
          icon: { label: 'test' },
          published: false,
          metadata: expect.objectContaining({
            multiselect: [
              {
                label: 'Country1',
                value: 'country_one',
              },
            ],
          }),
        })
      );
    });

    it('should return error if user does not have write permissions over entities', async () => {
      userFactory.mock({
        _id: 'user1',
        role: 'collaborator',
        groups: [],
      });
      try {
        await entities.multipleUpdate(
          ['shared1', 'other'],
          {
            published: false,
          },
          { language: 'en' }
        );
        fail('Should throw error');
      } catch (e) {
        expect(e.message).toContain('permissions');
      }
    });

    it('should update entities if user has permissions on them', async () => {
      userFactory.mock({
        _id: 'user2',
        role: 'collaborator',
        groups: [{ _id: 'group1' }],
      });

      const updated = await entities.multipleUpdate(
        ['shared1', 'other'],
        {
          title: 'test title',
        },
        { language: 'en' }
      );

      expect(updated.find(e => e.title !== 'test title')).toBeUndefined();
    });
  });

  describe('updateMetadataProperties', () => {
    let currentTemplate;
    beforeEach(() => {
      currentTemplate = {
        _id: templateChangingNames,
        properties: [
          { _id: templateChangingNamesProps.prop1id, type: 'text', name: 'property1' },
          { _id: templateChangingNamesProps.prop2id, type: 'text', name: 'property2' },
          { _id: templateChangingNamesProps.prop3id, type: 'text', name: 'property3' },
        ],
      };
    });

    it('should do nothing when there is no changed or deleted properties', async () => {
      jest.spyOn(entitiesModel, 'updateMany');

      await entities.updateMetadataProperties(currentTemplate, currentTemplate);
      expect(entitiesModel.updateMany).not.toHaveBeenCalled();
    });

    it('should update property names on entities based on the changes to the template', async () => {
      const template = {
        _id: templateChangingNames,
        properties: [
          {
            _id: templateChangingNamesProps.prop1id,
            type: 'text',
            name: 'property1',
            label: 'new name1',
          },
          {
            _id: templateChangingNamesProps.prop2id,
            type: 'text',
            name: 'property2',
            label: 'new name2',
          },
          {
            _id: templateChangingNamesProps.prop3id,
            type: 'text',
            name: 'property3',
            label: 'property3',
          },
        ],
      };

      await entities.updateMetadataProperties(template, currentTemplate);
      const [docs, docDiferentTemplate] = await Promise.all([
        entities.get({ template: templateChangingNames }),
        entities.getById('shared', 'en'),
      ]);
      expect(docs[0].metadata.new_name1).toEqual([{ value: 'value1' }]);
      expect(docs[0].metadata.new_name2).toEqual([{ value: 'value2' }]);
      expect(docs[0].metadata.property3).toEqual([{ value: 'value3' }]);
      expect(docs[1].metadata.new_name1).toEqual([{ value: 'value1' }]);
      expect(docs[1].metadata.new_name2).toEqual([{ value: 'value2' }]);
      expect(docs[1].metadata.property3).toEqual([{ value: 'value3' }]);
      expect(docDiferentTemplate.metadata.property1).toEqual([{ value: 'value1' }]);
      expect(search.indexEntities).toHaveBeenCalledWith({ template: template._id });
    });

    it('should delete and rename properties passed', async () => {
      const template = {
        _id: templateChangingNames,
        properties: [
          {
            _id: templateChangingNamesProps.prop2id,
            type: 'text',
            name: 'property2',
            label: 'new name',
          },
        ],
      };

      await entities.updateMetadataProperties(template, currentTemplate);
      const docs = await entities.get({ template: templateChangingNames });
      expect(docs[0].metadata.property1).not.toBeDefined();
      expect(docs[0].metadata.new_name).toEqual([{ value: 'value2' }]);
      expect(docs[0].metadata.property2).not.toBeDefined();
      expect(docs[0].metadata.property3).not.toBeDefined();
      expect(docs[1].metadata.property1).not.toBeDefined();
      expect(docs[1].metadata.new_name).toEqual([{ value: 'value2' }]);
      expect(docs[1].metadata.property2).not.toBeDefined();
      expect(docs[1].metadata.property3).not.toBeDefined();
    });

    it('should delete missing properties', async () => {
      const template = {
        _id: templateChangingNames,
        properties: [
          {
            _id: templateChangingNamesProps.prop2id,
            type: 'text',
            name: 'property2',
            label: 'property2',
          },
        ],
      };

      await entities.updateMetadataProperties(template, currentTemplate);
      const docs = await entities.get({ template: templateChangingNames });
      expect(docs[0].metadata.property1).not.toBeDefined();
      expect(docs[0].metadata.property2).toBeDefined();
      expect(docs[0].metadata.property3).not.toBeDefined();
      expect(docs[1].metadata.property1).not.toBeDefined();
      expect(docs[1].metadata.property2).toBeDefined();
      expect(docs[1].metadata.property3).not.toBeDefined();
    });
  });

  describe('removeValuesFromEntities', () => {
    it('should remove values of properties passed on all entities having that property', async () => {
      await entities.removeValuesFromEntities(['multiselect'], templateWithEntityAsThesauri);
      const _entities = await entities.get({ template: templateWithEntityAsThesauri });
      expect(_entities[0].metadata.multiselect).toEqual([]);
      expect(search.indexEntities).toHaveBeenCalled();
    });
  });

  describe('delete', () => {
    describe('when the original file does not exist', () => {
      it('should delete the entity and not throw an error', async () => {
        await entities.delete('shared1');
        const response = await entities.get({ sharedId: 'shared1' });
        expect(response.length).toBe(0);
      });
    });

    describe('when database deletion throws an error', () => {
      it('should reindex the documents', async () => {
        jest
          .spyOn(entitiesModel, 'delete')
          .mockImplementation(() => Promise.reject(new Error('error')));

        let error;
        try {
          await entities.delete('shared');
        } catch (_error) {
          error = _error;
          expect(search.indexEntities).toHaveBeenCalledWith({ sharedId: 'shared' }, '+fullText');
        }
        expect(error).toBeDefined();
        jest.mocked(entitiesModel.delete).mockRestore();
      });
    });

    it('should delete the document in the database', async () => {
      await entities.delete('shared');
      const response = await entities.get({ sharedId: 'shared' });
      expect(response.length).toBe(0);
    });

    it('should delete the document from the search', async () => {
      jest.mocked(search.delete).mockReset();
      await entities.delete('shared');
      const args = jest.mocked(search.delete).mock.calls;
      expect(search.delete).toHaveBeenCalled();
      expect(args[0][0]._id.toString()).toBe(batmanFinishesId.toString());
    });

    it('should delete the document relationships', async () => {
      await entities.delete('shared');
      const refs = await relationships.get({ entity: 'shared' });
      expect(refs.length).toBe(0);
    });

    it('should delete the original file', async () => {
      await fs.writeFile(uploadsPath('8202c463d6158af8065022d9b5014cc1.pdf'), '');
      await fs.writeFile(uploadsPath('8202c463d6158af8065022d9b5014ccb.pdf'), '');
      await fs.writeFile(uploadsPath('8202c463d6158af8065022d9b5014ccc.pdf'), '');
      await fs.writeFile(uploadsPath(`${uploadId1}.jpg`), '');
      await fs.writeFile(uploadsPath(`${uploadId2}.jpg`), '');

      expect(await storage.fileExists('8202c463d6158af8065022d9b5014ccb.pdf', 'document')).toBe(
        true
      );
      expect(await storage.fileExists('8202c463d6158af8065022d9b5014cc1.pdf', 'document')).toBe(
        true
      );
      expect(await storage.fileExists('8202c463d6158af8065022d9b5014ccc.pdf', 'document')).toBe(
        true
      );
      expect(await storage.fileExists(`${uploadId1}.jpg`, 'document')).toBe(true);
      expect(await storage.fileExists(`${uploadId2}.jpg`, 'document')).toBe(true);

      await entities.delete('shared');

      expect(await storage.fileExists('8202c463d6158af8065022d9b5014ccb.pdf', 'document')).toBe(
        false
      );
      expect(await storage.fileExists('8202c463d6158af8065022d9b5014cc1.pdf', 'document')).toBe(
        false
      );
      expect(await storage.fileExists('8202c463d6158af8065022d9b5014ccc.pdf', 'document')).toBe(
        false
      );

      expect(await storage.fileExists(`${uploadId1}.jpg`, 'document')).toBe(false);
      expect(await storage.fileExists(`${uploadId2}.jpg`, 'document')).toBe(false);
    });

    describe('when entity is being used as thesauri', () => {
      it('should delete the entity id on all entities using it from select/multiselect values', async () => {
        jest.mocked(search.indexEntities).mockRestore();
        await entities.delete('shared');

        const documentsToIndex = jest.mocked(search.bulkIndex).mock.calls[0][0];

        expect(documentsToIndex[0].metadata.multiselect).toEqual([{ value: 'value0' }]);
        expect(documentsToIndex[1].metadata.multiselect2).toEqual([{ value: 'value2' }]);
        expect(documentsToIndex[2].metadata.select).toEqual([]);
        expect(documentsToIndex[3].metadata.select2).toEqual([]);
      });

      describe('when there is no multiselects but there is selects', () => {
        it('should only delete selects and not throw an error', async () => {
          jest.mocked(search.indexEntities).mockRestore();
          jest.mocked(search.bulkIndex).mockReset();

          await entities.delete('shared10');

          const documentsToIndex = jest.mocked(search.bulkIndex).mock.calls[0][0];
          expect(documentsToIndex[0].metadata.select).toEqual([]);
        });
      });

      describe('when there is no selects but there is multiselects', () => {
        it('should only delete multiselects and not throw an error', async () => {
          jest.mocked(search.indexEntities).mockRestore();
          jest.mocked(search.bulkIndex).mockReset();
          await entities.delete('multiselect');
          const documentsToIndex = jest.mocked(search.bulkIndex).mock.calls[0][0];
          expect(documentsToIndex[0].metadata.multiselect).toEqual([{ value: 'value1' }]);
        });
      });
    });

    it('should delete the suggestions with the entity sharedId', async () => {
      const emitSpy = spyOnEmit();
      await entities.delete('shared');
      emitSpy.expectToEmitEvent(EntityDeletedEvent, {
        entity: fixtures.entities
          .filter(entity => entity.sharedId === 'shared')
          .map(entity => expect.objectContaining({ _id: entity._id })),
      });
      emitSpy.restore();
    });

    it('should remove the entity from the relationship metadata of related entities', async () => {
      await entities.delete('shared2');
      const [related] = await db.mongodb
        .collection('entities')
        .find({ sharedId: 'shared', title: 'Batman finishes' })
        .toArray();
      expect(related.metadata).toMatchObject({
        friends: [],
        enemies: [],
      });
      const [related2] = await db.mongodb
        .collection('entities')
        .find({ sharedId: 'entityWithOnlyAnyRelationship' })
        .toArray();
      expect(related2.metadata).toMatchObject({
        relationship_to_any_template: [],
      });
    });
  });

  describe('addLanguage()', () => {
    let createThumbnailSpy;

    beforeAll(async () => {
      createThumbnailSpy = jest.spyOn(entities, 'createThumbnail').mockImplementation(entity => {
        if (!entity.file) {
          return Promise.reject(
            new Error('entities without file should not try to create thumbnail')
          );
        }
        return Promise.resolve();
      });
    });

    afterAll(() => {
      createThumbnailSpy.mockRestore();
    });

    it('should duplicate all the entities from the default language to the new one', async () => {
      await entitiesModel.save({ _id: docId1, file: {} });

      await entities.addLanguage('ab', 2);
      const newEntities = await entities.get({ language: 'ab' }, '+permissions');
      expect(newEntities.length).toBe(15);

      const fromCheckPermissions = fixtures.entities.find(e => e.title === 'Unpublished entity ES');
      const toCheckPermissions = newEntities.find(e => e.title === 'Unpublished entity ES');
      expect(toCheckPermissions.permissions).toEqual(fromCheckPermissions.permissions);
    });

    it('should not try to add already existing languages', async () => {
      const oldCount = (await entities.get({ language: 'en' })).length;
      await entities.addLanguage('en');
      const newCount = (await entities.get({ language: 'en' })).length;
      expect(newCount).toBe(oldCount);
    });
  });

  describe('removeLanguage()', () => {
    it('should delete all entities from the language', async () => {
      jest.spyOn(search, 'deleteLanguage').mockImplementation(async () => Promise.resolve());
      jest.spyOn(entities, 'createThumbnail').mockImplementation(async () => Promise.resolve());
      await entities.addLanguage('ab');
      await entities.removeLanguage('ab');
      const newEntities = await entities.get({ language: 'ab' });

      expect(search.deleteLanguage).toHaveBeenCalledWith('ab');
      expect(newEntities.length).toBe(0);
    });
  });

  describe('validation', () => {
    it('should validate on save', async () => {
      const entity = {
        title: 'Test',
        template: templateId,
        metadata: { date: [{ value: 'invalid date' }] },
      };
      const options = { user: { _id: db.id() }, language: 'en' };

      try {
        await entities.save(entity, options);
        fail('should throw validation error');
      } catch (error) {
        expect(error).toBeInstanceOf(Ajv.ValidationError);
      }
    });
  });
});