huridocs/uwazi

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

Summary

Maintainability
C
7 hrs
Test Coverage
import { AuthorizationService } from 'api/authorization.v2/services/AuthorizationService';
import { getConnection } from 'api/common.v2/database/getConnectionForCurrentTenant';
import { MongoIdHandler } from 'api/common.v2/database/MongoIdGenerator';
import { partialImplementation } from 'api/common.v2/testing/partialImplementation';
import { MongoEntitiesDataSource } from 'api/entities.v2/database/MongoEntitiesDataSource';
import { MissingEntityError } from 'api/entities.v2/errors/entityErrors';
import { MongoFilesDataSource } from 'api/files.v2/database/MongoFilesDataSource';
import { MongoRelationshipsDataSource } from 'api/relationships.v2/database/MongoRelationshipsDataSource';
import { MongoRelationshipTypesDataSource } from 'api/relationshiptypes.v2/database/MongoRelationshipTypesDataSource';
import { MissingRelationshipTypeError } from 'api/relationshiptypes.v2/errors/relationshipTypeErrors';
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 { ObjectId } from 'mongodb';
import { DefaultTransactionManager } from 'api/common.v2/database/data_source_defaults';
import { CreateRelationshipService } from '../CreateRelationshipService';
import { DenormalizationService } from '../DenormalizationService';

const factory = getFixturesFactory();

const collectionInDb = (collection = 'relationships') => testingDB.mongodb?.collection(collection)!;

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

const validateAccessMock = jest.fn().mockResolvedValue(undefined);

const authServiceMock = partialImplementation<AuthorizationService>({
  validateAccess: validateAccessMock,
});

const denormalizeAfterCreatingRelationshipsMock = jest.fn().mockResolvedValue(undefined);

const denormalizationServiceMock = partialImplementation<DenormalizationService>({
  denormalizeAfterCreatingRelationships: denormalizeAfterCreatingRelationshipsMock,
});

const createService = () => {
  const connection = getConnection();
  const transactionManager = DefaultTransactionManager();
  const SettingsDataSource = new MongoSettingsDataSource(connection, transactionManager);

  validateAccessMock.mockReset();
  denormalizeAfterCreatingRelationshipsMock.mockReset();

  return new CreateRelationshipService(
    new MongoRelationshipsDataSource(connection, transactionManager),
    new MongoRelationshipTypesDataSource(connection, transactionManager),
    new MongoEntitiesDataSource(
      connection,
      new MongoTemplatesDataSource(connection, transactionManager),
      SettingsDataSource,
      transactionManager
    ),
    new MongoFilesDataSource(connection, transactionManager),
    transactionManager,
    MongoIdHandler,
    authServiceMock,
    denormalizationServiceMock
  );
};

const fixtures: DBFixture = {
  entities: [
    ...entityInLanguages(['en', 'hu'], 'entity1', 'template1'),
    ...entityInLanguages(['en', 'hu'], 'entity2', 'template2'),
    ...entityInLanguages(['en', 'hu'], 'entity3', 'template1'),
    ...entityInLanguages(['en', 'hu'], 'entity4', 'template3'),
  ],
  relationships: [],
  relationtypes: [
    {
      _id: factory.id('rel1'),
      name: 'rel1',
    },
    {
      _id: factory.id('rel2'),
      name: 'rel2',
    },
    {
      _id: factory.id('rel3'),
      name: 'rel3',
    },
    {
      _id: factory.id('rel4'),
      name: 'rel4',
    },
  ],
  files: [
    factory.fileDeprecated('file1', 'entity1', 'document', 'file1.pdf'),
    factory.fileDeprecated('file2', 'entity2', 'document', 'file2.pdf'),
    factory.fileDeprecated('file3', 'entity3', 'document', 'file3.pdf'),
  ],
  settings: [
    {
      languages: [
        {
          default: true,
          label: 'English',
          key: 'en',
        },
        {
          default: true,
          label: 'Hungarian',
          key: 'hu',
        },
      ],
    },
  ],
};

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

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

describe('create()', () => {
  it('should check for user write access in the involved entities', async () => {
    const service = createService();
    await service.create([
      {
        from: { type: 'entity', entity: 'entity1' },
        to: { type: 'entity', entity: 'entity2' },
        type: factory.id('rel1').toHexString(),
      },
      {
        from: { type: 'entity', entity: 'entity2' },
        to: { type: 'entity', entity: 'entity1' },
        type: factory.id('rel2').toHexString(),
      },
    ]);
    expect(validateAccessMock).toHaveBeenCalledWith(
      'write',
      expect.arrayContaining(['entity1', 'entity2'])
    );
  });

  describe('When the input is correct', () => {
    const execute = async () => {
      const service = createService();

      return service.create([
        {
          from: { type: 'entity', entity: 'entity1' },
          to: { type: 'entity', entity: 'entity2' },
          type: factory.id('rel1').toHexString(),
        },
        {
          from: {
            type: 'text',
            entity: 'entity2',
            file: factory.id('file2').toHexString(),
            text: 'text selection 2',
            selections: [{ page: 2, top: 2, left: 2, width: 2, height: 2 }],
          },
          to: { type: 'entity', entity: 'entity1' },
          type: factory.id('rel2').toHexString(),
        },
        {
          from: { type: 'entity', entity: 'entity3' },
          to: {
            type: 'text',
            entity: 'entity1',
            file: factory.id('file1').toHexString(),
            text: 'text selection 1',
            selections: [{ page: 1, top: 1, left: 1, width: 1, height: 1 }],
          },
          type: factory.id('rel3').toHexString(),
        },
      ]);
    };

    it('should return new connections', async () => {
      const relationships = await execute();

      expect(relationships).toEqual([
        {
          _id: expect.any(String),
          from: { entity: 'entity1' },
          to: { entity: 'entity2' },
          type: factory.id('rel1').toHexString(),
        },
        {
          _id: expect.any(String),
          from: {
            entity: 'entity2',
            file: factory.id('file2').toHexString(),
            selections: [{ page: 2, top: 2, left: 2, width: 2, height: 2 }],
            text: 'text selection 2',
          },
          to: { entity: 'entity1' },
          type: factory.id('rel2').toHexString(),
        },
        {
          _id: expect.any(String),
          from: { entity: 'entity3' },
          to: {
            entity: 'entity1',
            file: factory.id('file1').toHexString(),
            selections: [{ page: 1, top: 1, left: 1, width: 1, height: 1 }],
            text: 'text selection 1',
          },
          type: factory.id('rel3').toHexString(),
        },
      ]);

      expect(denormalizeAfterCreatingRelationshipsMock).toHaveBeenCalledWith(
        relationships.map(r => r._id)
      );
    });

    it('should persist new connections', async () => {
      await execute();

      const relationshipsInDb = await collectionInDb().find({}).sort({ from: 1 }).toArray();

      expect(relationshipsInDb).toEqual([
        {
          _id: expect.any(ObjectId),
          from: { entity: 'entity1' },
          to: { entity: 'entity2' },
          type: factory.id('rel1'),
        },
        {
          _id: expect.any(ObjectId),
          from: {
            entity: 'entity2',
            file: factory.id('file2'),
            selections: [{ page: 2, top: 2, left: 2, width: 2, height: 2 }],
            text: 'text selection 2',
          },
          to: { entity: 'entity1' },
          type: factory.id('rel2'),
        },
        {
          _id: expect.any(ObjectId),
          from: { entity: 'entity3' },
          to: {
            entity: 'entity1',
            file: factory.id('file1'),
            selections: [{ page: 1, top: 1, left: 1, width: 1, height: 1 }],
            text: 'text selection 1',
          },
          type: factory.id('rel3'),
        },
      ]);
    });

    it('should denormalize based on the newly created relationships', async () => {
      const created = await execute();

      expect(denormalizeAfterCreatingRelationshipsMock).toHaveBeenCalledWith(
        created.map(c => c._id)
      );
    });
  });

  describe('When an entity does not exist', () => {
    it('should throw a validation error', async () => {
      const service = createService();
      try {
        await service.create([
          {
            from: { type: 'entity', entity: 'entity1' },
            to: { type: 'entity', entity: 'entity2' },
            type: factory.id('rel1').toHexString(),
          },
          {
            from: { type: 'entity', entity: 'entity2' },
            to: { type: 'entity', entity: 'non-existing' },
            type: factory.id('rel2').toHexString(),
          },
          {
            from: { type: 'entity', entity: 'entity3' },
            to: { type: 'entity', entity: 'entity1' },
            type: factory.id('rel3').toHexString(),
          },
        ]);
        fail('should throw error');
      } catch (e) {
        await expect(e.message).toMatch(/existing/);
        expect(e).toBeInstanceOf(MissingEntityError);
      }
      expect(denormalizeAfterCreatingRelationshipsMock).not.toHaveBeenCalled();
    });
  });

  describe('When a type does not exist', () => {
    it('should throw a validation error', async () => {
      const service = createService();
      try {
        await service.create([
          {
            from: { type: 'entity', entity: 'entity1' },
            to: { type: 'entity', entity: 'entity2' },
            type: factory.id('rel1').toHexString(),
          },
          {
            from: { type: 'entity', entity: 'entity2' },
            to: { type: 'entity', entity: 'entity1' },
            type: factory.id('invalid').toHexString(),
          },
        ]);
        fail('should throw error');
      } catch (e) {
        await expect(e.message).toMatch(/existing/);
        expect(e).toBeInstanceOf(MissingRelationshipTypeError);
      }
      expect(denormalizeAfterCreatingRelationshipsMock).not.toHaveBeenCalled();
    });
  });

  describe('When a file does not exist', () => {
    it('should throw a validation error', async () => {
      const service = createService();
      try {
        await service.create([
          {
            from: {
              type: 'text',
              entity: 'entity1',
              file: factory.id('some file').toHexString(),
              text: 'some text',
              selections: [{ page: 1, top: 1, left: 1, width: 1, height: 1 }],
            },
            to: { type: 'entity', entity: 'entity2' },
            type: factory.id('rel1').toHexString(),
          },
        ]);
        fail('should throw error');
      } catch (e) {
        await expect(e.message).toMatch(/file/i);
      }
      expect(denormalizeAfterCreatingRelationshipsMock).not.toHaveBeenCalled();
    });
  });

  it('should not fail if the input is empty', async () => {
    const service = createService();

    const result = await service.create([]);

    expect(result).toEqual([]);
  });
});