huridocs/uwazi

View on GitHub
app/api/relationships.v2/services/specs/DenormalizationService.spec.ts

Summary

Maintainability
F
4 days
Test Coverage
import { getConnection } from 'api/common.v2/database/getConnectionForCurrentTenant';
import { MongoEntitiesDataSource } from 'api/entities.v2/database/MongoEntitiesDataSource';
import { MongoRelationshipsDataSource } from 'api/relationships.v2/database/MongoRelationshipsDataSource';
import { MongoSettingsDataSource } from 'api/settings.v2/database/MongoSettingsDataSource';
import { MongoTemplatesDataSource } from 'api/templates.v2/database/MongoTemplatesDataSource';
import { getFixturesFactory } from 'api/utils/fixturesFactory';
import { testingEnvironment } from 'api/utils/testingEnvironment';
import testingDB, { DBFixture } from 'api/utils/testing_db';
import { Db } from 'mongodb';
import { partialImplementation } from 'api/common.v2/testing/partialImplementation';
import { DefaultTransactionManager } from 'api/common.v2/database/data_source_defaults';
import { DenormalizationService } from '../DenormalizationService';
import { RelationshipPropertyUpdateStrategy } from '../propertyUpdateStrategies/RelationshipPropertyUpdateStrategy';

const factory = getFixturesFactory();

const entityInLanguages = (langs: string[], id: string, template?: string) =>
  langs.map(lang => factory.entity(id, template, {}, { language: lang }));

const fixtures: DBFixture = {
  relationships: [
    {
      _id: factory.id('rel1'),
      from: { entity: 'entity1' },
      to: { entity: 'hub1' },
      type: factory.id('nullType'),
    },
    {
      _id: factory.id('rel2'),
      to: { entity: 'hub1' },
      from: { entity: 'entity3' },
      type: factory.id('relType1'),
    },
    {
      _id: factory.id('rel3'),
      to: { entity: 'hub1' },
      from: {
        entity: 'entity4',
        file: factory.id('file4'),
        selections: [{ page: 1, top: 1, left: 1, height: 1, width: 1 }],
        text: '',
      },
      type: factory.id('relType1'),
    },
    {
      _id: factory.id('rel4'),
      from: { entity: 'entity1' },
      to: { entity: 'hub2' },
      type: factory.id('nullType'),
    },
    {
      _id: factory.id('rel5'),
      to: { entity: 'hub2' },
      from: { entity: 'entity5' },
      type: factory.id('relType2'),
    },
    {
      _id: factory.id('rel6'),
      to: { entity: 'hub2' },
      from: { entity: 'entity6' },
      type: factory.id('relType3'),
    },
    {
      _id: factory.id('rel7'),
      from: { entity: 'entity2' },
      to: { entity: 'hub3' },
      type: factory.id('relType4'),
    },
    {
      _id: factory.id('rel8'),
      to: { entity: 'hub3' },
      from: { entity: 'entity7' },
      type: factory.id('relType5'),
    },
    {
      _id: factory.id('rel9'),
      from: { entity: 'entity7' },
      to: { entity: 'entity4' },
      type: factory.id('relType5'),
    },
    {
      _id: factory.id('rel10'),
      from: { entity: 'entity9' },
      to: { entity: 'entity4' },
      type: factory.id('relType5'),
    },
  ],
  entities: [
    ...factory.entityInMultipleLanguages(
      ['hu', 'es'],
      'entity1',
      'template1',
      {},
      { obsoleteMetadata: [] },
      {
        hu: {
          metadata: {
            relationshipProp1: [{ value: 'entity4', label: 'entity4-hu' }],
            relationshipProp1_with_inherit: [
              {
                value: 'entity4',
                label: 'entity4-hu',
                inheritedType: 'text',
                inheritedValue: [{ value: 'inherited_value_hu' }],
              },
            ],
          },
        },
        es: {
          metadata: {
            relationshipProp1: [{ value: 'entity4', label: 'entity4-es' }],
            relationshipProp1_with_inherit: [
              {
                value: 'entity4',
                label: 'entity4-es',
                inheritedType: 'text',
                inheritedValue: [{ value: 'inherited_value_es' }],
              },
            ],
          },
        },
      }
    ),
    ...entityInLanguages(['hu', 'es'], 'entity2'),
    ...entityInLanguages(['hu', 'es'], 'hub1', 'formerHubsTemplate'),
    ...entityInLanguages(['hu', 'es'], 'entity3', 'template2'),
    ...factory.entityInMultipleLanguages(
      ['hu', 'es'],
      'entity4',
      'template4',
      {},
      { obsoleteMetadata: [] },
      {
        hu: {
          metadata: {
            to_inherit: [{ value: 'inherited_value_hu' }],
          },
        },
        es: {
          metadata: {
            to_inherit: [{ value: 'inherited_value_es' }],
          },
        },
      }
    ),
    ...entityInLanguages(['hu', 'es'], 'hub2', 'formerHubsTemplate'),
    ...entityInLanguages(['hu', 'es'], 'entity5', 'template2'),
    ...entityInLanguages(['hu', 'es'], 'entity6', 'template3'),
    ...entityInLanguages(['hu', 'es'], 'hub3'),
    ...entityInLanguages(['hu', 'es'], 'entity7', 'template7'),
    ...entityInLanguages(['hu', 'es'], 'entity8'),
    ...entityInLanguages(['hu', 'es'], 'entity9', 'template7'),
    ...entityInLanguages(['hu', 'es'], 'entity10', 'template1'),
  ],
  files: [factory.fileDeprecated('file4', 'entity4', 'document', 'file4.pdf', 'hu')],
  templates: [
    factory.template('formerHubsTemplate', [
      {
        name: 'connectedEntitiesProp',
        type: 'newRelationship',
        label: 'connectedEntitiesProp',
        query: [
          {
            types: [factory.id('nullType')],
            direction: 'in',
            match: [
              {
                templates: [],
                traverse: [],
              },
            ],
          },
        ],
      },
    ]),
    factory.template('template1', [
      {
        name: 'relationshipProp1',
        type: 'newRelationship',
        label: 'relationshipProp1',
        query: [
          {
            types: [factory.id('nullType')],
            direction: 'out',
            match: [
              {
                templates: [factory.id('formerHubsTemplate')],
                traverse: [
                  {
                    types: [factory.id('relType1')],
                    direction: 'in',
                    match: [
                      {
                        templates: [factory.id('template4')],
                      },
                    ],
                  },
                ],
              },
            ],
          },
        ],
      },
      {
        name: 'relationshipProp1_with_inherit',
        type: 'newRelationship',
        label: 'relationshipProp1_with_inherit',
        denormalizedProperty: 'to_inherit',
        query: [
          {
            types: [factory.id('nullType')],
            direction: 'out',
            match: [
              {
                templates: [factory.id('formerHubsTemplate')],
                traverse: [
                  {
                    types: [factory.id('relType1')],
                    direction: 'in',
                    match: [
                      {
                        templates: [factory.id('template4')],
                      },
                    ],
                  },
                ],
              },
            ],
          },
        ],
      },
    ]),
    factory.template('template7', [
      {
        name: 'relationshipProp2',
        type: 'newRelationship',
        label: 'relationshipProp2',
        query: [
          {
            types: [factory.id('relType5')],
            direction: 'out',
            match: [
              {
                templates: [factory.id('template4')],
                traverse: [
                  {
                    types: [factory.id('relType1')],
                    direction: 'out',
                    match: [
                      {
                        templates: [factory.id('formerHubsTemplate')],
                      },
                    ],
                  },
                ],
              },
            ],
          },
        ],
      },
    ]),
    factory.template('template4', [
      {
        name: 'to_inherit',
        type: 'text',
        label: 'to_inherit',
      },
      {
        name: 'relationshipProp3',
        type: 'newRelationship',
        label: 'relationshipProp3',
        query: [
          {
            types: [factory.id('relType1')],
            direction: 'out',
            match: [
              {
                templates: [factory.id('formerHubsTemplate')],
                traverse: [
                  {
                    types: [factory.id('nullType')],
                    direction: 'in',
                    match: [
                      {
                        templates: [factory.id('template1')],
                        traverse: [
                          {
                            types: [factory.id('nullType')],
                            direction: 'in',
                            match: [
                              {
                                templates: [factory.id('formerHubsTemplate')],
                              },
                            ],
                          },
                        ],
                      },
                    ],
                  },
                ],
              },
            ],
          },
        ],
      },
    ]),
  ],
  settings: [
    {
      languages: [
        {
          default: true,
          label: 'Hungarian',
          key: 'hu',
        },
        {
          label: 'Spanish',
          key: 'es',
        },
      ],
    },
  ],
};

let db: Db;
let service: DenormalizationService;
let indexMock: jest.Mock;
let updateMock: jest.Mock;
let updateByTemplateMock: jest.Mock;

let triggerCommit: () => Promise<unknown>;

beforeEach(async () => {
  await testingEnvironment.setUp(fixtures);

  indexMock = jest.fn();
  updateMock = jest.fn();
  updateByTemplateMock = jest.fn();

  db = getConnection();
  const transactionManager = DefaultTransactionManager();
  triggerCommit = async () => transactionManager.executeOnCommitHandlers(undefined);
  const relationshipsDataSource = new MongoRelationshipsDataSource(db, transactionManager);
  const templatesDataSource = new MongoTemplatesDataSource(db, transactionManager);
  const entitiesDataSource = new MongoEntitiesDataSource(
    db,
    templatesDataSource,
    new MongoSettingsDataSource(db, transactionManager),
    transactionManager
  );
  service = new DenormalizationService(
    relationshipsDataSource,
    entitiesDataSource,
    templatesDataSource,
    new MongoSettingsDataSource(db, transactionManager),
    transactionManager,
    indexMock,
    partialImplementation<RelationshipPropertyUpdateStrategy>({
      update: updateMock,
      updateByTemplate: updateByTemplateMock,
    })
  );
});

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

describe('denormalizeAfterCreatingRelationships()', () => {
  describe('when executing on a newly created relationship', () => {
    it.each(['hu', 'es'])(
      'should mark the relationship fields as invalid in the entities in "%s"',
      async language => {
        await service.denormalizeAfterCreatingRelationships([factory.id('rel3').toHexString()]);
        const entities = await testingDB.mongodb
          ?.collection('entities')
          .find({ language, 'obsoleteMetadata.0': { $exists: true } })
          .toArray();

        expect(entities).toEqual(
          expect.arrayContaining([
            expect.objectContaining({
              sharedId: 'entity1',
              obsoleteMetadata: expect.arrayContaining([
                'relationshipProp1',
                'relationshipProp1_with_inherit',
              ]),
            }),
            expect.objectContaining({
              sharedId: 'entity4',
              obsoleteMetadata: ['relationshipProp3'],
            }),
            expect.objectContaining({
              sharedId: 'entity7',
              obsoleteMetadata: ['relationshipProp2'],
            }),
            expect.objectContaining({
              sharedId: 'entity9',
              obsoleteMetadata: ['relationshipProp2'],
            }),
            expect.objectContaining({
              sharedId: 'hub1',
              obsoleteMetadata: ['connectedEntitiesProp'],
            }),
          ])
        );
        expect(entities?.length).toBe(5);

        await triggerCommit();

        expect(updateMock).toHaveBeenCalledWith(
          expect.arrayContaining(['entity1', 'entity4', 'entity7', 'entity9', 'hub1'])
        );
        expect(indexMock).not.toHaveBeenCalled();
      }
    );
  });
});

describe('denormalizeBeforeDeletingFiles()', () => {
  describe('when executing before deleting a file', () => {
    it.each(['hu', 'es'])(
      'should mark the relationship fields as invalid in the entities in "%s"',
      async language => {
        await service.denormalizeBeforeDeletingFiles([factory.id('file4').toHexString()]);
        const entities = await testingDB.mongodb
          ?.collection('entities')
          .find({ language, 'obsoleteMetadata.0': { $exists: true } })
          .toArray();
        expect(entities).toEqual(
          expect.arrayContaining([
            expect.objectContaining({
              sharedId: 'entity1',
              obsoleteMetadata: expect.arrayContaining([
                'relationshipProp1',
                'relationshipProp1_with_inherit',
              ]),
            }),
            expect.objectContaining({
              sharedId: 'entity4',
              obsoleteMetadata: ['relationshipProp3'],
            }),
            expect.objectContaining({
              sharedId: 'entity7',
              obsoleteMetadata: ['relationshipProp2'],
            }),
            expect.objectContaining({
              sharedId: 'entity9',
              obsoleteMetadata: ['relationshipProp2'],
            }),
            expect.objectContaining({
              sharedId: 'hub1',
              obsoleteMetadata: ['connectedEntitiesProp'],
            }),
          ])
        );
        expect(entities?.length).toBe(5);

        await triggerCommit();

        expect(updateMock).toHaveBeenCalledWith(
          expect.arrayContaining(['entity1', 'entity4', 'entity7', 'entity9', 'hub1'])
        );
        expect(indexMock).not.toHaveBeenCalled();
      }
    );
  });
});

describe('denormalizeAfterUpdatingEntities()', () => {
  describe('when executing on an existing entity', () => {
    it('should update the relationship fields denormalizations in the entity with the new data in the provided language', async () => {
      await testingDB.mongodb?.collection('entities').updateOne(
        { sharedId: 'entity4', language: 'es' },
        {
          $set: {
            title: 'entity4-es-edited',
            'metadata.to_inherit': [{ value: 'inherited_value_es_edited' }],
          },
        }
      );

      await service.denormalizeAfterUpdatingEntities(['entity4'], 'es');

      const entities = await testingDB.mongodb
        ?.collection('entities')
        .find({ sharedId: 'entity1' })
        .toArray();
      expect(entities).toMatchObject([
        {
          sharedId: 'entity1',
          language: 'hu',
          metadata: {
            relationshipProp1: [{ value: 'entity4', label: 'entity4-hu' }],
            relationshipProp1_with_inherit: [
              {
                value: 'entity4',
                label: 'entity4-hu',
                inheritedType: 'text',
                inheritedValue: [{ value: 'inherited_value_hu' }],
              },
            ],
          },
          obsoleteMetadata: [],
        },
        {
          sharedId: 'entity1',
          language: 'es',
          metadata: {
            relationshipProp1: [{ value: 'entity4', label: 'entity4-es-edited' }],
            relationshipProp1_with_inherit: [
              {
                value: 'entity4',
                label: 'entity4-es-edited',
                inheritedType: 'text',
                inheritedValue: [{ value: 'inherited_value_es_edited' }],
              },
            ],
          },
          obsoleteMetadata: [],
        },
      ]);

      await triggerCommit();

      expect(updateMock).not.toHaveBeenCalled();
      expect(indexMock).toHaveBeenCalledWith(['entity1', 'entity1']);
    });
  });
});

describe('denormalizeAfterCreatingOrUpdatingProperty()', () => {
  it('should denormalize all the entities of the template', async () => {
    await service.denormalizeAfterCreatingOrUpdatingProperty(
      factory.id('template1').toHexString(),
      ['relationshipProp1', 'relationshipProp1_with_inherit']
    );

    const entities = await testingDB.mongodb
      ?.collection('entities')
      .find({ template: factory.id('template1') })
      .toArray();

    expect(entities).toMatchObject([
      {
        sharedId: 'entity1',
        language: 'hu',
        obsoleteMetadata: ['relationshipProp1', 'relationshipProp1_with_inherit'],
      },
      {
        sharedId: 'entity1',
        language: 'es',
        obsoleteMetadata: ['relationshipProp1', 'relationshipProp1_with_inherit'],
      },
      {
        sharedId: 'entity10',
        language: 'hu',
        obsoleteMetadata: ['relationshipProp1', 'relationshipProp1_with_inherit'],
      },
      {
        sharedId: 'entity10',
        language: 'es',
        obsoleteMetadata: ['relationshipProp1', 'relationshipProp1_with_inherit'],
      },
    ]);

    await triggerCommit();
    expect(indexMock).not.toHaveBeenCalled();
    expect(updateByTemplateMock).toHaveBeenCalledWith(factory.id('template1').toHexString());
  });
});