huridocs/uwazi

View on GitHub
app/api/entities/specs/validateEntity.spec.ts

Summary

Maintainability
A
1 hr
Test Coverage
/* eslint-disable max-lines,max-statements */

import { ErrorObject } from 'ajv';
import ValidationError from 'ajv/dist/runtime/validation_error';
import db from 'api/utils/testing_db';
import { propertyTypes } from 'shared/propertyTypes';
import { EntitySchema } from 'shared/types/entityType';
import templates from 'api/templates';
import { TemplateSchema } from 'shared/types/templateType';
import * as entitiesIndex from 'api/search/entitiesIndex';
import fixtures, { templateId, simpleTemplateId, nonExistentId } from './validatorFixtures';

import { customErrorMessages } from '../validation/metadataValidators.js';
import { validateEntity } from '../validateEntity';

describe('validateEntity', () => {
  beforeEach(async () => {
    jest.spyOn(entitiesIndex, 'updateMapping').mockImplementation(async () => Promise.resolve());
    //@ts-ignore
    await db.setupFixturesAndContext(fixtures);
  });

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

  describe('validateEntity', () => {
    const createEntity = (entity: {}): EntitySchema => ({
      _id: 'entity',
      sharedId: 'sharedId',
      title: 'Test',
      template: templateId.toString(),
      language: 'en',
      mongoLanguage: 'en',
      metadata: {},
      ...entity,
    });

    const testValid = async (entity: EntitySchema) => {
      try {
        await validateEntity(entity);
      } catch (e) {
        if (e instanceof ValidationError) {
          throw JSON.stringify(e, null, ' ');
        }
        throw e;
      }
    };

    const expectError = async (
      entity: EntitySchema,
      message: string,
      instancePath: string,
      restOfError: Partial<ErrorObject> = {}
    ) => {
      await expect(validateEntity(entity)).rejects.toHaveProperty(
        'errors',
        expect.arrayContaining([expect.objectContaining({ instancePath, message, ...restOfError })])
      );
    };

    it('should allow ObjectId for _id fields', async () => {
      const entity = createEntity({
        _id: db.id(),
        user: db.id(),
        template: templateId,
      });
      await testValid(entity);
    });

    it('should allow removing the icon', async () => {
      const entity = createEntity({ icon: { _id: null, type: 'Empty' } });
      await testValid(entity);
    });

    it('should allow template to be missing', async () => {
      let entity = createEntity({ template: undefined });
      await testValid(entity);
      entity = createEntity({ template: '' });
      await testValid(entity);
    });

    it('should fail if template does not exist', async () => {
      const entity = createEntity({ template: nonExistentId.toString() });
      await expectError(entity, 'template does not exist', '.template');
    });

    it('should fail if title is not a string', async () => {
      let entity = createEntity({ title: {} });
      await expectError(entity, expect.any(String), '/title');
      entity = createEntity({ title: 10 });
      await expectError(entity, expect.any(String), '/title');
    });

    it('should fail if title exceeds the lucene term byte-length limit', async () => {
      const entity = createEntity({
        title: Math.random().toString(36).repeat(20000),
      });
      await expectError(entity, expect.any(String), '/title');
    });

    it('should allow title to be missing', async () => {
      const entity = createEntity({ title: undefined });
      await testValid(entity);
    });

    describe('metadata', () => {
      const largeField = Math.random().toString(36).repeat(20000);

      it('should not allow metadata keys that are not defined on the template properties', async () => {
        const entity = createEntity({
          metadata: {
            not_allowed_property: [],
            not_allowed_property2: [],
            name: [],
          },
        });

        await expectError(
          entity,
          customErrorMessages.property_not_allowed,
          ".metadata['not_allowed_property']"
        );

        await expectError(
          entity,
          customErrorMessages.property_not_allowed,
          ".metadata['not_allowed_property2']"
        );
      });

      it('should allow non-required properties to be missing', async () => {
        const entity = createEntity({
          metadata: {},
        });
        await testValid(entity);
      });

      describe('if no property is required', () => {
        it('should allow metadata object to be missing if there are not required properties', async () => {
          const entity = createEntity({ metadata: undefined });
          await testValid(entity);
        });

        it('should allow metadata object to be empty', async () => {
          const entity = createEntity({ template: simpleTemplateId, metadata: {} });
          await testValid(entity);
        });
      });

      describe('if property is required', () => {
        it('should fail if field does not exist', async () => {
          const template: TemplateSchema = {
            name: 'template with required props',
            properties: [
              { label: 'name', name: 'name', required: true, type: 'text' },
              { label: 'markdown', name: 'markdown', required: true, type: 'markdown' },
              { label: 'numeric', name: 'numeric', required: true, type: 'numeric' },
            ],
            commonProperties: [{ name: 'title', label: 'title', type: 'text' }],
          };
          const templateWithRequiredProps = await templates.save(template, 'en');

          let entity = createEntity({ template: templateWithRequiredProps._id });
          await expectError(entity, customErrorMessages.required, ".metadata['name']");
          entity = createEntity({
            template: templateWithRequiredProps._id,
            metadata: { name: [{ value: '' }] },
          });
          await expectError(entity, customErrorMessages.required, ".metadata['name']");
          entity = createEntity({
            template: templateWithRequiredProps._id,
            metadata: { name: [{ value: null }] },
          });
          await expectError(entity, customErrorMessages.required, ".metadata['name']");
          entity = createEntity({
            template: templateWithRequiredProps._id,
            metadata: { markdown: [] },
          });
          await expectError(entity, customErrorMessages.required, ".metadata['markdown']");
          entity = createEntity({
            template: templateWithRequiredProps._id,
            metadata: {
              name: [{ value: 'name' }],
              markdown: [{ value: 'markdown' }],
              numeric: [{ value: 0 }],
            },
          });
          await validateEntity(entity);
        });
      });

      describe('any property', () => {
        it('should fail if value is not an array', async () => {
          const entity = createEntity({ metadata: { name: { value: 10 } } });
          await expectError(entity, 'must be array', '/metadata/name');
        });
      });

      describe('text property', () => {
        it('should fail if value is not a single string', async () => {
          let entity = createEntity({ metadata: { name: [{ value: 'a' }, { value: 'b' }] } });
          await expectError(entity, customErrorMessages[propertyTypes.text], ".metadata['name']");
          entity = createEntity({ metadata: { name: [{ value: 10 }] } });
          await expectError(entity, customErrorMessages[propertyTypes.text], ".metadata['name']");
        });
      });

      describe('markdown property', () => {
        it('should fail if value is not a string', async () => {
          const entity = createEntity({ metadata: { markdown: [{ value: 345 }] } });
          await expectError(
            entity,
            customErrorMessages[propertyTypes.markdown],
            ".metadata['markdown']"
          );
        });
        it('should NOT fail if value is a string that exceeds the lucene term byte-length limit', async () => {
          const entity = createEntity({ metadata: { markdown: [{ value: largeField }] } });
          await validateEntity(entity);
        });
      });

      describe('media property', () => {
        it('should fail if value is not a string', async () => {
          const entity = createEntity({ metadata: { media: [{ value: 10 }] } });
          await expectError(entity, customErrorMessages[propertyTypes.media], ".metadata['media']");
        });
      });

      describe('image property', () => {
        it('should fail if value is not a string', async () => {
          const entity = createEntity({ metadata: { image: [{ value: 10 }] } });
          await expectError(entity, customErrorMessages[propertyTypes.image], ".metadata['image']");
        });
      });

      describe('numeric property', () => {
        it('should fail if value is not a number', async () => {
          const entity = createEntity({ metadata: { numeric: [{ value: 'test' }] } });
          await expectError(
            entity,
            customErrorMessages[propertyTypes.numeric],
            ".metadata['numeric']"
          );
        });
        it('should allow value to be empty string', async () => {
          const entity = createEntity({ metadata: { numeric: [{ value: '' }] } });
          await testValid(entity);
        });
        it('should allow numbers passed as strings', async () => {
          const entity = createEntity({ metadata: { numeric: [{ value: '55.5' }] } });
          await testValid(entity);
        });
      });

      describe('date property', () => {
        it('should fail if value is not a number', async () => {
          let entity = createEntity({ metadata: { date: [{ value: 'test' }] } });
          await expectError(entity, customErrorMessages[propertyTypes.date], ".metadata['date']");
          entity = createEntity({ metadata: { date: [{ value: -100 }] } });
          await testValid(entity);
        });

        it('should allow value to be null if property is not required', async () => {
          const entity = createEntity({ metadata: { date: [{ value: null }] } });
          await testValid(entity);
        });
      });

      describe('multidate property', () => {
        it('should fail if value is not an array of numbers', async () => {
          let entity = createEntity({
            metadata: { multidate: [{ value: 100 }, { value: '200' }, { value: -5 }] },
          });
          await expectError(
            entity,
            customErrorMessages[propertyTypes.multidate],
            ".metadata['multidate']"
          );
          entity = createEntity({ metadata: { multidate: [{ value: '100' }] } });
          await expectError(
            entity,
            customErrorMessages[propertyTypes.multidate],
            ".metadata['multidate']"
          );
        });

        it('should allow null items', async () => {
          const entity = createEntity({
            metadata: {
              multidate: [{ value: 100 }, { value: null }, { value: 200 }, { value: null }],
            },
          });
          await testValid(entity);
        });
      });

      describe('daterange property', () => {
        it('should fail if value is not an object', async () => {
          let entity = createEntity({ metadata: { daterange: [{ value: 'dates' }] } });
          await expectError(
            entity,
            customErrorMessages[propertyTypes.daterange],
            ".metadata['daterange']"
          );
          entity = createEntity({
            metadata: { daterange: [{ value: 100 }, { value: 200 }, { value: -5 }] },
          });
          await expectError(
            entity,
            customErrorMessages[propertyTypes.daterange],
            ".metadata['daterange']"
          );
        });

        it('should allow either from or to to be null', async () => {
          let entity = createEntity({
            metadata: { daterange: [{ value: { from: null, to: 100 } }] },
          });
          await testValid(entity);
          entity = createEntity({ metadata: { daterange: [{ value: { from: 100, to: null } }] } });
          await testValid(entity);
          entity = createEntity({ metadata: { daterange: [{ value: { from: null, to: -100 } }] } });
          await testValid(entity);
          entity = createEntity({ metadata: { daterange: [{ value: { from: null, to: null } }] } });
          await testValid(entity);
        });

        it('should fail if from is greater than to', async () => {
          const entity = createEntity({
            metadata: { daterange: [{ value: { from: 100, to: 50 } }] },
          });
          await expectError(
            entity,
            customErrorMessages[propertyTypes.daterange],
            ".metadata['daterange']"
          );
        });
      });

      describe('multidaterange property', () => {
        it('should fail if value is not array of date ranges', async () => {
          let entity = createEntity({
            metadata: { multidaterange: [{ value: 100 }, { value: 200 }] },
          });
          await expectError(
            entity,
            customErrorMessages[propertyTypes.multidaterange],
            ".metadata['multidaterange']"
          );
          entity = createEntity({
            metadata: { multidaterange: [{ value: { from: 200, to: 100 } }] },
          });
          await expectError(
            entity,
            customErrorMessages[propertyTypes.multidaterange],
            ".metadata['multidaterange']"
          );
          entity = createEntity({
            metadata: { multidaterange: [{ value: { from: -200, to: -100 } }] },
          });
          await testValid(entity);
        });
      });

      describe('select property', () => {
        it('should fail if value is not string', async () => {
          const entity = createEntity({ metadata: { select: [{ value: 55 }] } });
          await expectError(
            entity,
            customErrorMessages[propertyTypes.select],
            ".metadata['select']"
          );
        });

        it('should allow empty string if property is not required', async () => {
          const entity = createEntity({ metadata: { markdown: [{ value: '' }] } });
          await testValid(entity);
        });

        it('should not allow foreign ids that do not exists', async () => {
          let entity = createEntity({
            metadata: {
              select: [{ value: 'non_existent_thesauri1' }],
            },
          });

          await expectError(
            entity,
            customErrorMessages.dictionary_wrong_foreing_id,
            ".metadata['select']",
            {
              data: [{ value: 'non_existent_thesauri1' }],
            }
          );

          entity = createEntity({
            metadata: {
              select: [{ value: 'dic1-value1' }],
            },
          });
          await testValid(entity);
        });

        it('should not allow as foreign id a root value of a nested thesaurus', async () => {
          const entity = createEntity({
            metadata: {
              required_multiselect: [{ value: '1' }],
            },
          });

          await expectError(
            entity,
            customErrorMessages.dictionary_wrong_foreing_id,
            ".metadata['required_multiselect']",
            {
              data: [{ value: '1' }],
            }
          );
        });
      });

      describe('multiselect property', () => {
        it('should fail if value is an empty string', async () => {
          const entity = createEntity({ metadata: { multiselect: [{ value: '' }] } });
          await expectError(
            entity,
            customErrorMessages[propertyTypes.multiselect],
            ".metadata['multiselect']"
          );
        });

        it('should allow value to be an empty array', async () => {
          const entity = createEntity({ metadata: { multiselect: [] } });
          await testValid(entity);
        });

        it('should not allow foreign ids that do not exists', async () => {
          const entity = createEntity({
            metadata: {
              multiselect: [
                { value: 'dic1-value1' },
                { value: 'dic2-value2' },
                { value: 'non_existent_thesauri' },
              ],
            },
          });

          await expectError(
            entity,
            customErrorMessages.dictionary_wrong_foreing_id,
            ".metadata['multiselect']",
            {
              data: [{ value: 'dic1-value1' }, { value: 'non_existent_thesauri' }],
            }
          );
        });
      });

      describe('relationship property', () => {
        it('should fail if value is empty string', async () => {
          const entity = createEntity({
            metadata: { relationship: [{ value: '' }, { value: '' }] },
          });
          await expectError(
            entity,
            customErrorMessages[propertyTypes.relationship],
            ".metadata['relationship']"
          );
        });

        it('should not allow foreign ids that do not exists', async () => {
          const entity = createEntity({
            metadata: {
              relationship: [
                { value: 'entity1' },
                { value: 'non_existent_entity' },
                { value: 'non_existent_entity2' },
              ],
            },
          });

          await expectError(
            entity,
            customErrorMessages.relationship_wrong_foreign_id,
            ".metadata['relationship']",
            {
              data: [{ value: 'non_existent_entity' }, { value: 'non_existent_entity2' }],
            }
          );
        });

        it('should not allow foreign ids that do not belong to diferent template', async () => {
          const entity = createEntity({
            language: 'es',
            metadata: {
              relationship: [{ value: 'entity1' }, { value: 'entity2' }, { value: 'entity3' }],
            },
          });

          await expectError(
            entity,
            customErrorMessages.relationship_wrong_foreign_id,
            ".metadata['relationship']",
            {
              data: [{ value: 'entity2' }],
            }
          );
        });

        it('should fail if relationship fields with the same configuration have different values', async () => {
          const entity = createEntity({
            language: 'es',
            metadata: {
              relationship: [{ value: 'entity1' }, { value: 'entity3' }],
              relationship2: [{ value: 'entity1' }],
            },
          });

          await expectError(
            entity,
            customErrorMessages.relationship_values_should_match,
            ".metadata['relationship2']"
          );
        });
      });

      describe('relationship property V2', () => {
        it('should fail if value is empty string', async () => {
          const entity = createEntity({
            metadata: { newRelationship: [{ value: '' }, { value: '' }] },
          });
          await expectError(
            entity,
            customErrorMessages[propertyTypes.newRelationship],
            ".metadata['newRelationship']"
          );
        });

        it('should not allow foreign ids that do not exists', async () => {
          const entity = createEntity({
            metadata: {
              newRelationship: [
                { value: 'entity1' },
                { value: 'non_existent_entity' },
                { value: 'non_existent_entity2' },
              ],
            },
          });

          await expectError(
            entity,
            customErrorMessages.relationship_wrong_foreign_id,
            ".metadata['newRelationship']",
            {
              data: [{ value: 'non_existent_entity' }, { value: 'non_existent_entity2' }],
            }
          );
        });

        it('should not allow foreign ids that belong to diferent template', async () => {
          const entity = createEntity({
            language: 'es',
            metadata: {
              newRelationship: [{ value: 'entity1' }, { value: 'entity2' }, { value: 'entity3' }],
            },
          });

          await expectError(
            entity,
            customErrorMessages.relationship_wrong_foreign_id,
            ".metadata['newRelationship']",
            {
              data: [{ value: 'entity2' }],
            }
          );
        });

        it('should not allow to change values if it is read-only', async () => {
          const entity = createEntity({
            language: 'es',
            metadata: {
              newRelationship2: [{ value: 'entity1' }],
            },
          });

          await expectError(
            entity,
            customErrorMessages.read_only,
            ".metadata['newRelationship2']",
            {
              data: [{ value: 'entity1' }],
            }
          );
        });
      });

      describe('link property', () => {
        it('should fail if value is not an object', async () => {
          const entity = createEntity({ metadata: { link: [{ value: 'bad_link' }] } });
          await expectError(entity, customErrorMessages[propertyTypes.link], ".metadata['link']");
        });

        it('should fail if url is not provided', async () => {
          const entity = createEntity({
            metadata: { link: [{ value: { label: 'label', url: '' } }] },
          });
          await expectError(entity, customErrorMessages[propertyTypes.link], ".metadata['link']");
        });

        it('should be ok if both are empty', async () => {
          const entity = createEntity({ metadata: { link: [{ value: { label: '', url: '' } }] } });
          await testValid(entity);
        });
        it('should be ok if label is empty', async () => {
          const entity = createEntity({
            metadata: { link: [{ value: { label: '', url: 'https://youtube.com' } }] },
          });
          await testValid(entity);
        });
      });

      describe('geolocation property', () => {
        it('should fail if value is not an array of lat/lon object', async () => {
          const entity = createEntity({
            metadata: { geolocation: [{ value: 'bad_geo' }] },
          });
          await expectError(
            entity,
            customErrorMessages[propertyTypes.geolocation],
            ".metadata['geolocation']"
          );
        });

        it('should fail if label is not a string', async () => {
          const entity = createEntity({
            metadata: { geolocation: [{ value: { lat: 10, lon: 10, label: 10 } }] },
          });
          await expectError(entity, 'must be string', '/metadata/geolocation/0/value');
        });

        it('should not fail if label is not present', async () => {
          const entity = createEntity({
            metadata: { geolocation: [{ value: { label: undefined, lat: 10, lon: 10 } }] },
          });
          await testValid(entity);
        });

        it('should fail if lat is not within range -90 - 90', async () => {
          let entity = createEntity({
            metadata: { geolocation: [{ value: { label: undefined, lat: -91, lon: 10 } }] },
          });
          await expect(validateEntity(entity)).rejects.toHaveProperty(
            'errors',
            expect.arrayContaining([
              expect.objectContaining({ instancePath: '/metadata/geolocation/0/value' }),
            ])
          );
          entity = createEntity({
            metadata: { geolocation: [{ value: { label: undefined, lat: 91, lon: 10 } }] },
          });
          await expect(validateEntity(entity)).rejects.toHaveProperty(
            'errors',
            expect.arrayContaining([
              expect.objectContaining({ instancePath: '/metadata/geolocation/0/value' }),
            ])
          );
        });

        it('should fail if lon is not within range -180 - 180', async () => {
          let entity = createEntity({
            metadata: { geolocation: [{ value: { label: undefined, lat: 10, lon: -181 } }] },
          });
          await expect(validateEntity(entity)).rejects.toHaveProperty(
            'errors',
            expect.arrayContaining([
              expect.objectContaining({ instancePath: '/metadata/geolocation/0/value' }),
            ])
          );
          entity = createEntity({
            metadata: { geolocation: [{ value: { label: undefined, lat: 181, lon: 10 } }] },
          });
          await expect(validateEntity(entity)).rejects.toHaveProperty(
            'errors',
            expect.arrayContaining([
              expect.objectContaining({ instancePath: '/metadata/geolocation/0/value' }),
            ])
          );
        });
      });
    });
  });
});