app/api/suggestions/specs/suggestions.spec.ts
/* eslint-disable max-statements */
import db from 'api/utils/testing_db';
import {
EntitySuggestionType,
IXSuggestionStateType,
IXSuggestionType,
IXSuggestionsFilter,
} from 'shared/types/suggestionType';
import { ObjectId } from 'mongodb';
import { Suggestions } from '../suggestions';
import {
factory,
file2Id,
file3Id,
fixtures,
personTemplateId,
shared2enId,
shared2esId,
suggestionId,
shared2AgeSuggestionId,
selectAcceptanceFixtureBase,
relationshipAcceptanceFixtureBase,
} from './fixtures';
const getSuggestions = async (filter: IXSuggestionsFilter, size = 5) =>
Suggestions.get(filter, { page: { size, number: 1 } });
const findOneSuggestion = async (query: any): Promise<IXSuggestionType> =>
db.mongodb
?.collection('ixsuggestions')
.findOne({ ...query }) as unknown as Promise<IXSuggestionType>;
const stateUpdateCases: {
state: Partial<IXSuggestionStateType>;
reason: string;
suggestionQuery: any;
}[] = [
{
state: { obsolete: true },
reason: 'obsolete, if the suggestion is older than the model',
suggestionQuery: { entityId: 'shared5', propertyName: 'age' },
},
{
state: {
withValue: true,
withSuggestion: false,
labeled: false,
match: false,
obsolete: false,
processing: false,
error: false,
},
reason: 'if entity value exists, file label is empty, suggestion is empty',
suggestionQuery: { entityId: 'shared3', propertyName: 'age' },
},
{
state: {
labeled: true,
withValue: true,
withSuggestion: true,
match: true,
obsolete: false,
processing: false,
error: false,
},
reason: 'if file label exists, suggestion and entity value exist and match',
suggestionQuery: {
entityId: 'shared2',
propertyName: 'super_powers',
language: 'en',
status: 'ready',
},
},
{
state: {
labeled: true,
withValue: true,
withSuggestion: true,
match: true,
obsolete: false,
processing: false,
error: false,
},
reason:
'when property is a date, and if file label exists, suggestion and entity value exist and match',
suggestionQuery: {
entityId: 'shared7',
propertyName: 'first_encountered',
language: 'es',
},
},
{
state: {
labeled: false,
withValue: false,
withSuggestion: false,
match: false,
obsolete: false,
processing: false,
error: false,
},
reason: 'if entity value, file label, suggestion are all empty',
suggestionQuery: {
entityId: 'shared8',
propertyName: 'enemy',
language: 'en',
},
},
{
state: {
labeled: true,
withValue: true,
withSuggestion: false,
match: false,
obsolete: false,
processing: false,
error: false,
},
reason: 'if entity value and file label exists, suggestion is empty',
suggestionQuery: {
entityId: 'shared6',
propertyName: 'enemy',
language: 'en',
fileId: { $exists: true },
},
},
{
state: {
labeled: true,
withValue: true,
withSuggestion: false,
match: false,
obsolete: false,
processing: false,
error: false,
},
reason:
'when property is a date, and if entity value and file label exists, suggestion is empty',
suggestionQuery: {
entityId: 'shared7',
propertyName: 'first_encountered',
language: 'en',
},
},
{
state: {
labeled: true,
withValue: true,
withSuggestion: true,
match: false,
obsolete: false,
processing: false,
error: false,
},
reason: 'if file label exists, suggestion and entity value exist but do not match',
suggestionQuery: {
propertyName: 'super_powers',
language: 'es',
},
},
{
state: {
labeled: true,
withValue: true,
withSuggestion: true,
match: false,
obsolete: false,
processing: false,
error: false,
},
reason:
'when property is a date, if file label exists, suggestion and entity value exist but do not match',
suggestionQuery: {
entityId: 'shared7',
propertyName: 'first_encountered',
language: 'pr',
},
},
{
state: {
labeled: false,
withValue: true,
withSuggestion: true,
match: true,
obsolete: false,
processing: false,
error: false,
},
reason: 'if file label is empty, but suggestion and entity value exist and match',
suggestionQuery: {
entityId: 'shared1',
propertyName: 'enemy',
},
},
{
state: {
labeled: false,
withValue: true,
withSuggestion: true,
match: true,
obsolete: false,
processing: false,
error: false,
},
reason:
'when property is a date, and if file label is empty, but suggestion and entity value exist and match',
suggestionQuery: {
entityId: 'shared8',
propertyName: 'first_encountered',
language: 'en',
},
},
{
state: {
labeled: false,
withValue: true,
withSuggestion: true,
match: false,
obsolete: false,
processing: false,
error: false,
},
reason: 'if file label is empty, suggestion and entity value exist but do not match',
suggestionQuery: {
entityId: 'shared6',
propertyName: 'enemy',
language: 'en',
},
},
{
reason: 'selects not labeled, if the entity does not have a value',
suggestionQuery: {
entityId: 'entityWithSelects3',
propertyName: 'property_select',
},
state: { labeled: false },
},
{
reason: 'selects labeled, if the entity has a value',
suggestionQuery: {
entityId: 'entityWithSelects',
propertyName: 'property_select',
},
state: { labeled: true },
},
{
reason: 'selects always with context',
suggestionQuery: {
entityId: 'entityWithSelects3',
propertyName: 'property_select',
},
state: { hasContext: true },
},
{
reason: 'selects as match if the value matches the suggestion',
suggestionQuery: {
entityId: 'entityWithSelects',
propertyName: 'property_select',
},
state: { match: true },
},
{
reason: 'selects as not match if the value does not match the suggestion',
suggestionQuery: {
entityId: 'entityWithSelects2',
propertyName: 'property_select',
},
state: { match: false },
},
{
reason: 'multiselects not labeled, if the entity does not have a value',
suggestionQuery: {
entityId: 'entityWithSelects3',
propertyName: 'property_multiselect',
},
state: { labeled: false, withValue: false },
},
{
reason: 'multiselects labeled, if the entity has a value',
suggestionQuery: {
entityId: 'entityWithSelects',
propertyName: 'property_multiselect',
},
state: { labeled: true },
},
{
reason: 'multiselects always with context',
suggestionQuery: {
entityId: 'entityWithSelects3',
propertyName: 'property_multiselect',
},
state: { hasContext: true },
},
{
reason: 'multiselects as match if the set of values strictly matches the suggestion',
suggestionQuery: {
entityId: 'entityWithSelects',
propertyName: 'property_multiselect',
},
state: { match: true },
},
{
reason: 'multiselects as not match if the set of values does not strictly match the suggestion',
suggestionQuery: {
entityId: 'entityWithSelects2',
propertyName: 'property_multiselect',
},
state: { match: false },
},
];
const newProcessingSuggestion: IXSuggestionType = {
entityId: 'new_processing_suggestion',
entityTemplate: personTemplateId.toString(),
propertyName: 'new',
extractorId: factory.id('new_extractor'),
suggestedValue: 'new',
segment: 'Some new segment',
language: 'en',
date: 5,
page: 2,
status: 'processing',
error: '',
};
const newErroringSuggestion: IXSuggestionType = {
entityId: 'new_erroring_suggestion',
entityTemplate: personTemplateId.toString(),
propertyName: 'new',
extractorId: factory.id('new_extractor'),
suggestedValue: 'new',
segment: 'Some new segment',
language: 'en',
date: 5,
page: 2,
status: 'failed',
error: 'Some error message',
};
const matchState = (match: boolean = true): IXSuggestionStateType => ({
labeled: true,
withValue: true,
withSuggestion: true,
match,
hasContext: true,
obsolete: false,
processing: false,
error: false,
});
type SuggestionBase = Pick<
IXSuggestionType,
| 'fileId'
| 'entityId'
| 'entityTemplate'
| 'propertyName'
| 'extractorId'
| 'date'
| 'status'
| 'error'
>;
const prepareAndAcceptSuggestion = async (
suggestionBase: SuggestionBase,
suggestedValue: string | string[],
language: string,
propertyName: string,
extractorName: string,
acceptanceParameters: {
addedValues?: string[];
removedValues?: string[];
} = {}
) => {
const suggestion = {
...suggestionBase,
suggestedValue,
language,
};
await Suggestions.save(suggestion);
const savedSuggestion = (await Suggestions.get({ extractorId: factory.id(extractorName) }, {}))
.suggestions[0];
const { _id, sharedId, entityId } = savedSuggestion;
await Suggestions.accept([{ _id, sharedId, entityId, ...acceptanceParameters }]);
const acceptedSuggestion = (await Suggestions.get({ extractorId: factory.id(extractorName) }, {}))
.suggestions[0];
const entities = await db.mongodb?.collection('entities').find({ sharedId }).toArray();
const metadataValues = entities?.map(entity => entity.metadata[propertyName]);
const allFiles = await db.mongodb?.collection('files').find({}).toArray();
return { acceptedSuggestion, metadataValues, allFiles };
};
const selectSuggestionBase = (propertyName: string, extractorName: string): SuggestionBase => ({
fileId: factory.id('fileForentityWithSelects'),
entityId: 'entityWithSelects',
entityTemplate: factory.id('templateWithSelects').toString(),
propertyName,
extractorId: factory.id(extractorName),
date: 5,
status: 'ready' as 'ready',
error: '',
});
const prepareAndAcceptSelectSuggestion = async (
suggestedValue: string | string[],
language: string,
propertyName: string,
extractorName: string,
acceptanceParameters: {
addedValues?: string[];
removedValues?: string[];
} = {}
) =>
prepareAndAcceptSuggestion(
selectSuggestionBase(propertyName, extractorName),
suggestedValue,
language,
propertyName,
extractorName,
acceptanceParameters
);
const relationshipSuggestionBase = (
propertyName: string,
extractorName: string
): SuggestionBase => ({
fileId: factory.id('fileForEntityWithRelationships'),
entityId: 'entityWithRelationships_sId',
entityTemplate: factory.id('rel_template').toString(),
propertyName,
extractorId: factory.id(extractorName),
date: 5,
status: 'ready' as 'ready',
error: '',
});
const prepareAndAcceptRelationshipSuggestion = async (
suggestedValue: string | string[],
language: string,
propertyName: string,
extractorName: string,
acceptanceParameters: {
addedValues?: string[];
removedValues?: string[];
} = {}
) =>
prepareAndAcceptSuggestion(
relationshipSuggestionBase(propertyName, extractorName),
suggestedValue,
language,
propertyName,
extractorName,
acceptanceParameters
);
describe('suggestions', () => {
afterAll(async () => {
await db.disconnect();
});
describe('get()', () => {
beforeEach(async () => {
await db.setupFixturesAndContext(fixtures);
await Suggestions.updateStates({});
});
it('should not fail on 0 suggestions', async () => {
const { suggestions } = await Suggestions.get(
{ extractorId: factory.id('non_existing_extractor').toString() },
{ page: { size: 50, number: 1 } }
);
expect(suggestions.length).toBe(0);
});
it('should return all title suggestions', async () => {
const { suggestions } = await Suggestions.get(
{ extractorId: factory.id('title_extractor').toString() },
{ page: { size: 50, number: 1 } }
);
expect(suggestions.length).toBe(6);
});
it('should return total page count', async () => {
const { totalPages } = await Suggestions.get(
{ extractorId: factory.id('title_extractor').toString() },
{ page: { size: 50, number: 1 } }
);
expect(totalPages).toBe(1);
});
it('should be able to filter', async () => {
const { suggestions } = await Suggestions.get(
{
extractorId: factory.id('super_powers_extractor').toString(),
},
{ page: { size: 50, number: 1 } }
);
expect(suggestions.length).toBe(3);
});
it('should return suggestion and extra entity information', async () => {
const { suggestions } = await Suggestions.get(
{ extractorId: factory.id('super_powers_extractor').toString() },
{ page: { size: 50, number: 1 } }
);
expect(suggestions).toMatchObject([
{
fileId: file2Id,
propertyName: 'super_powers',
extractorId: factory.id('super_powers_extractor'),
suggestedValue: 'scientific knowledge',
segment: 'he relies on his own scientific knowledge',
language: 'en',
date: 4,
page: 5,
currentValue: 'scientific knowledge',
labeledValue: 'scientific knowledge',
state: {
labeled: true,
withValue: true,
withSuggestion: true,
match: true,
hasContext: true,
obsolete: false,
processing: false,
error: false,
},
entityId: shared2enId,
sharedId: 'shared2',
entityTitle: 'Batman en',
},
{
fileId: file3Id,
propertyName: 'super_powers',
extractorId: factory.id('super_powers_extractor'),
suggestedValue: 'scientific knowledge es',
segment: 'el confía en su propio conocimiento científico',
language: 'es',
date: 4,
page: 5,
currentValue: 'conocimiento científico',
labeledValue: 'conocimiento científico',
state: {
labeled: true,
withValue: true,
withSuggestion: true,
match: false,
hasContext: true,
obsolete: false,
processing: false,
error: false,
},
entityId: shared2esId,
sharedId: 'shared2',
entityTitle: 'Batman es',
},
{
fileId: factory.id('F7'),
propertyName: 'super_powers',
extractorId: factory.id('super_powers_extractor'),
segment: 'he puts up with Bruce Wayne',
currentValue: 'no super powers',
date: 4000,
page: 3,
entityId: factory.id('Alfred-english-entity'),
entityTemplateId: personTemplateId,
entityTitle: 'Alfred',
error: '',
labeledValue: 'no super powers',
language: 'en',
sharedId: 'shared3',
state: {
error: false,
hasContext: true,
labeled: true,
match: false,
obsolete: false,
processing: false,
withSuggestion: true,
withValue: true,
},
suggestedValue: 'puts up with Bruce Wayne',
},
]);
});
it('should return suggestion and extra entity information (multiselects)', async () => {
const { suggestions } = await Suggestions.get(
{ extractorId: factory.id('multiselect_extractor').toString() },
{ page: { size: 50, number: 1 } }
);
expect(suggestions).toMatchObject([
{
fileId: factory.id('fileForentityWithSelects'),
sharedId: 'entityWithSelects',
propertyName: 'property_multiselect',
extractorId: factory.id('multiselect_extractor'),
suggestedValue: ['A', '1A'],
language: 'en',
date: 5,
currentValue: ['A', '1A'],
entityTitle: 'entityWithSelects',
},
{
fileId: factory.id('fileForentityWithSelects2'),
sharedId: 'entityWithSelects2',
propertyName: 'property_multiselect',
extractorId: factory.id('multiselect_extractor'),
suggestedValue: ['A', '1B'],
language: 'en',
date: 5,
currentValue: ['A', '1A'],
entityTitle: 'entityWithSelects2',
},
{
fileId: factory.id('fileForentityWithSelects3'),
sharedId: 'entityWithSelects3',
propertyName: 'property_multiselect',
extractorId: factory.id('multiselect_extractor'),
suggestedValue: ['A', '1A'],
language: 'en',
date: 5,
currentValue: [],
entityTitle: 'entityWithSelects3',
},
]);
});
it('should return match status', async () => {
const { suggestions: superPowersSuggestions } = await getSuggestions({
extractorId: factory.id('super_powers_extractor').toString(),
});
expect(
superPowersSuggestions.find((s: EntitySuggestionType) => s.language === 'en').state
).toEqual({
labeled: true,
withValue: true,
withSuggestion: true,
match: true,
hasContext: true,
obsolete: false,
processing: false,
error: false,
});
const { suggestions: enemySuggestions } = await getSuggestions(
{ extractorId: factory.id('enemy_extractor').toString() },
6
);
expect(
enemySuggestions.filter(
(s: EntitySuggestionType) => s.sharedId === 'shared6' && s.language === 'en'
)[1].state
).toEqual({
labeled: false,
withValue: true,
withSuggestion: true,
match: false,
hasContext: true,
obsolete: false,
processing: false,
error: false,
});
expect(
enemySuggestions.find((s: EntitySuggestionType) => s.sharedId === 'shared1').state
).toEqual({
labeled: false,
withValue: true,
withSuggestion: true,
match: true,
hasContext: true,
obsolete: false,
processing: false,
error: false,
});
expect(
enemySuggestions.find(
(s: EntitySuggestionType) => s.sharedId === 'shared8' && s.language === 'en'
).state
).toEqual({
labeled: false,
withValue: false,
withSuggestion: false,
match: false,
hasContext: true,
obsolete: false,
processing: false,
error: false,
});
const { suggestions: ageSuggestions } = await getSuggestions({
extractorId: factory.id('age_extractor').toString(),
});
expect(ageSuggestions.length).toBe(5);
expect(
ageSuggestions.find((s: EntitySuggestionType) => s.sharedId === 'shared5').state.obsolete
).toEqual(true);
expect(
ageSuggestions.find((s: EntitySuggestionType) => s.sharedId === 'shared3').state
).toEqual({
labeled: false,
withValue: true,
withSuggestion: false,
match: false,
hasContext: true,
obsolete: false,
processing: false,
error: false,
});
});
it('should return mismatch status', async () => {
const { suggestions: superPowersSuggestions } = await getSuggestions({
extractorId: factory.id('super_powers_extractor').toString(),
});
expect(
superPowersSuggestions.find((s: EntitySuggestionType) => s.language === 'es').state
).toEqual({
labeled: true,
withValue: true,
withSuggestion: true,
match: false,
hasContext: true,
obsolete: false,
processing: false,
error: false,
});
const { suggestions: enemySuggestions } = await getSuggestions({
extractorId: factory.id('enemy_extractor').toString(),
});
expect(
enemySuggestions.find(
(s: EntitySuggestionType) => s.sharedId === 'shared6' && s.language === 'en'
).state
).toEqual({
labeled: true,
withValue: true,
withSuggestion: false,
match: false,
hasContext: true,
obsolete: false,
processing: false,
error: false,
});
expect(
enemySuggestions.find(
(s: EntitySuggestionType) => s.sharedId === 'shared9' && s.language === 'en'
).state
).toEqual({
labeled: false,
withValue: false,
withSuggestion: true,
match: false,
hasContext: true,
obsolete: false,
processing: false,
error: false,
});
});
it('should return error status', async () => {
const { suggestions } = await getSuggestions({
extractorId: factory.id('age_extractor').toString(),
});
expect(
suggestions.find((s: EntitySuggestionType) => s.sharedId === 'shared4').state.error
).toBe(true);
});
});
describe('accept()', () => {
describe('general', () => {
beforeAll(async () => {
await db.setupFixturesAndContext(fixtures);
await Suggestions.updateStates({});
});
it('should accept suggestions', async () => {
const { suggestions } = await getSuggestions({
extractorId: factory.id('super_powers_extractor').toString(),
});
const labelMismatchedSuggestions = suggestions.filter(
(sug: any) => sug.state.labeled && !sug.state.match
);
const ids = new Set(labelMismatchedSuggestions.map((sug: any) => sug._id.toString()));
await Suggestions.accept(
labelMismatchedSuggestions.map((sug: any) => ({
_id: sug._id,
sharedId: sug.sharedId,
entityId: sug.entityId,
}))
);
const { suggestions: newSuggestions } = await getSuggestions({
extractorId: factory.id('super_powers_extractor').toString(),
});
const changedSuggestions = newSuggestions.filter((sug: any) => ids.has(sug._id.toString()));
expect(changedSuggestions).toMatchObject([
{
_id: labelMismatchedSuggestions[0]._id,
state: matchState(),
suggestedValue: labelMismatchedSuggestions[0].suggestedValue,
labeledValue: labelMismatchedSuggestions[0].suggestedValue,
},
{
_id: labelMismatchedSuggestions[1]._id,
state: matchState(),
suggestedValue: labelMismatchedSuggestions[1].suggestedValue,
labeledValue: labelMismatchedSuggestions[1].suggestedValue,
},
]);
});
it('should require all suggestions to come from the same extractor', async () => {
const [ageSuggestion] = (await getSuggestions({ extractorId: factory.id('age_extractor') }))
.suggestions;
const [superPowersSuggestion] = (
await getSuggestions({
extractorId: factory.id('super_powers_extractor'),
})
).suggestions;
await expect(
Suggestions.accept([
{
_id: ageSuggestion._id,
sharedId: ageSuggestion.sharedId,
entityId: ageSuggestion.entityId,
},
{
_id: superPowersSuggestion._id,
sharedId: superPowersSuggestion.sharedId,
entityId: superPowersSuggestion.entityId,
},
])
).rejects.toThrow('All suggestions must come from the same extractor');
});
it('should not accept a suggestion with an error', async () => {
const { suggestions } = await getSuggestions({
extractorId: factory.id('age_extractor').toString(),
});
const errorSuggestion = suggestions.find(
(s: EntitySuggestionType) => s.sharedId === 'shared4'
);
try {
await Suggestions.accept([
{
_id: errorSuggestion._id,
sharedId: errorSuggestion.sharedId,
entityId: errorSuggestion.entityId,
},
]);
} catch (e: any) {
expect(e?.message).toBe('Some Suggestions have an error.');
}
});
});
describe('numeric/date', () => {
beforeAll(async () => {
await db.setupFixturesAndContext(fixtures);
await Suggestions.updateStates({});
});
it('should update entities of all languages if property name is numeric or date', async () => {
const { suggestions } = await getSuggestions({
extractorId: factory.id('age_extractor').toString(),
});
const suggestionsToAccept = suggestions.filter(
sug => sug.sharedId === 'shared2' || sug.sharedId === 'shared1'
);
await Suggestions.accept([
{
_id: suggestionsToAccept[0]._id,
sharedId: suggestionsToAccept[0].sharedId,
entityId: suggestionsToAccept[0].entityId,
},
{
_id: suggestionsToAccept[1]._id,
sharedId: suggestionsToAccept[1].sharedId,
entityId: suggestionsToAccept[1].entityId,
},
]);
const entities1 = await db.mongodb
?.collection('entities')
.find({ sharedId: 'shared1' })
.toArray();
const ages1 = entities1?.map(entity => entity.metadata.age[0].value);
expect(ages1).toEqual([17, 17]);
const entities2 = await db.mongodb
?.collection('entities')
.find({ sharedId: 'shared2' })
.toArray();
const ages2 = entities2?.map(entity => entity.metadata.age[0].value);
expect(ages2).toEqual([20, 20, 20]);
});
});
describe('select', () => {
beforeEach(async () => {
await db.setupFixturesAndContext(selectAcceptanceFixtureBase);
});
it('should validate that the id exists in the dictionary', async () => {
const action = async () => {
await prepareAndAcceptSelectSuggestion('Z', 'en', 'property_select', 'select_extractor');
};
await expect(action()).rejects.toThrow('Id is invalid: Z (Nested Thesaurus).');
});
it('should update entities of all languages, with the properly translated labels', async () => {
const { acceptedSuggestion, metadataValues, allFiles } =
await prepareAndAcceptSelectSuggestion('A', 'en', 'property_select', 'select_extractor');
expect(acceptedSuggestion.state).toEqual(matchState());
expect(metadataValues).toEqual([
[{ value: 'A', label: 'A' }],
[{ value: 'A', label: 'Aes' }],
]);
expect(allFiles).toEqual(selectAcceptanceFixtureBase.files);
});
it('should handle grouped values', async () => {
const { acceptedSuggestion, metadataValues, allFiles } =
await prepareAndAcceptSelectSuggestion('1A', 'en', 'property_select', 'select_extractor');
expect(acceptedSuggestion.state).toEqual(matchState());
expect(metadataValues).toEqual([
[{ value: '1A', label: '1A', parent: { value: '1', label: '1' } }],
[{ value: '1A', label: '1Aes', parent: { value: '1', label: '1es' } }],
]);
expect(allFiles).toEqual(selectAcceptanceFixtureBase.files);
});
});
describe('multiselect', () => {
beforeEach(async () => {
await db.setupFixturesAndContext(selectAcceptanceFixtureBase);
});
it('should validate that the ids exist in the dictionary', async () => {
const action = async () => {
await prepareAndAcceptSelectSuggestion(
['Z', '1A', 'Y', 'A'],
'en',
'property_multiselect',
'multiselect_extractor'
);
};
await expect(action()).rejects.toThrow('Ids are invalid: Z, Y (Nested Thesaurus).');
});
it('should validate that partial acceptance is allowed only for multiselects/relationships', async () => {
const addAction = async () => {
await prepareAndAcceptSelectSuggestion(
'1A',
'en',
'property_select',
'select_extractor',
{
addedValues: ['1A'],
}
);
};
await expect(addAction()).rejects.toThrow(
'Partial acceptance is only allowed for multiselects or relationships.'
);
const removeAction = async () => {
await prepareAndAcceptSelectSuggestion(
'1A',
'en',
'property_select',
'select_extractor',
{
removedValues: ['1B'],
}
);
};
await expect(removeAction()).rejects.toThrow(
'Partial acceptance is only allowed for multiselects or relationships.'
);
});
it("should validate that the accepted id's through partial acceptance do exist on the suggestion", async () => {
const action = async () => {
await prepareAndAcceptSelectSuggestion(
['1A', '1B'],
'en',
'property_multiselect',
'multiselect_extractor',
{
addedValues: ['1A', 'Y', 'Z'],
}
);
};
await expect(action()).rejects.toThrow(
'Some of the accepted values do not exist in the suggestion: Y, Z. Cannot accept values that are not suggested.'
);
});
it("should validate that the id's to remove through partial acceptance do not exist on the suggestion", async () => {
const action = async () => {
await prepareAndAcceptSelectSuggestion(
['1A', '1B'],
'en',
'property_multiselect',
'multiselect_extractor',
{
removedValues: ['1A', 'A'],
}
);
};
await expect(action()).rejects.toThrow(
'Some of the removed values exist in the suggestion: 1A. Cannot remove values that are suggested.'
);
});
it('should allow full acceptance, and update entites of all languages, with the properly translated labels', async () => {
const { acceptedSuggestion, metadataValues, allFiles } =
await prepareAndAcceptSelectSuggestion(
['1A', '1B'],
'en',
'property_multiselect',
'multiselect_extractor'
);
expect(acceptedSuggestion.state).toEqual(matchState());
expect(metadataValues).toEqual([
[
{ value: '1A', label: '1A', parent: { value: '1', label: '1' } },
{ value: '1B', label: '1B', parent: { value: '1', label: '1' } },
],
[
{ value: '1A', label: '1Aes', parent: { value: '1', label: '1es' } },
{ value: '1B', label: '1Bes', parent: { value: '1', label: '1es' } },
],
]);
expect(allFiles).toEqual(selectAcceptanceFixtureBase.files);
});
it('should allow partial acceptance, and update entites of all languages, with the properly translated labels', async () => {
const { acceptedSuggestion, metadataValues, allFiles } =
await prepareAndAcceptSelectSuggestion(
['B', '1B'],
'en',
'property_multiselect',
'multiselect_extractor',
{
addedValues: ['B'],
}
);
expect(acceptedSuggestion.state).toEqual(matchState(false));
expect(metadataValues).toEqual([
[
{ value: 'A', label: 'A' },
{ value: '1A', label: '1A', parent: { value: '1', label: '1' } },
{ value: 'B', label: 'B' },
],
[
{ value: 'A', label: 'Aes' },
{ value: '1A', label: '1Aes', parent: { value: '1', label: '1es' } },
{ value: 'B', label: 'Bes' },
],
]);
expect(allFiles).toEqual(selectAcceptanceFixtureBase.files);
});
it('should do nothing on partial acceptance if the id is already in the entity metadata', async () => {
const { acceptedSuggestion, metadataValues, allFiles } =
await prepareAndAcceptSelectSuggestion(
['1A', '1B'],
'en',
'property_multiselect',
'multiselect_extractor',
{
addedValues: ['1A'],
}
);
expect(acceptedSuggestion.state).toEqual(matchState(false));
expect(metadataValues).toEqual([
[
{ value: 'A', label: 'A' },
{ value: '1A', label: '1A', parent: { value: '1', label: '1' } },
],
[
{ value: 'A', label: 'Aes' },
{ value: '1A', label: '1Aes', parent: { value: '1', label: '1es' } },
],
]);
expect(allFiles).toEqual(selectAcceptanceFixtureBase.files);
});
it('should allow removal through partial acceptance, and update entities of all languages', async () => {
const { acceptedSuggestion, metadataValues, allFiles } =
await prepareAndAcceptSelectSuggestion(
['1A', '1B'],
'en',
'property_multiselect',
'multiselect_extractor',
{
removedValues: ['A'],
}
);
expect(acceptedSuggestion.state).toEqual(matchState(false));
expect(metadataValues).toEqual([
[{ value: '1A', label: '1A', parent: { value: '1', label: '1' } }],
[{ value: '1A', label: '1Aes', parent: { value: '1', label: '1es' } }],
]);
expect(allFiles).toEqual(selectAcceptanceFixtureBase.files);
});
it('should do nothing on removal through partial acceptance if the id is not in the entity metadata', async () => {
const { acceptedSuggestion, metadataValues, allFiles } =
await prepareAndAcceptSelectSuggestion(
['1A', 'A'],
'en',
'property_multiselect',
'multiselect_extractor',
{
removedValues: ['B'],
}
);
expect(acceptedSuggestion.state).toEqual(matchState());
expect(metadataValues).toEqual([
[
{ value: 'A', label: 'A' },
{ value: '1A', label: '1A', parent: { value: '1', label: '1' } },
],
[
{ value: 'A', label: 'Aes' },
{ value: '1A', label: '1Aes', parent: { value: '1', label: '1es' } },
],
]);
expect(allFiles).toEqual(selectAcceptanceFixtureBase.files);
});
});
describe('relationship', () => {
beforeEach(async () => {
await db.setupFixturesAndContext(relationshipAcceptanceFixtureBase);
});
it('should validate that the entities in the suggestion exist', async () => {
const action = async () => {
await prepareAndAcceptRelationshipSuggestion(
['S1_sId', 'X_sId', 'S2_sId', 'Y_sId'],
'en',
'relationship_to_source',
'relationship_extractor'
);
};
await expect(action()).rejects.toThrow(
'The following sharedIds do not exist in the database: X_sId, Y_sId.'
);
});
it("should validate that the accepted id's through partial acceptance do exist on the suggestion", async () => {
const action = async () => {
await prepareAndAcceptRelationshipSuggestion(
['S1_sId', 'S2_sId'],
'en',
'relationship_to_source',
'relationship_extractor',
{
addedValues: ['S1_sId', 'X_sId', 'Y_sId'],
}
);
};
await expect(action()).rejects.toThrow(
'Some of the accepted values do not exist in the suggestion: X_sId, Y_sId. Cannot accept values that are not suggested.'
);
});
it("should validate that the id's to remove through partial acceptance do not exist on the suggestion", async () => {
const action = async () => {
await prepareAndAcceptRelationshipSuggestion(
['S1_sId', 'S2_sId'],
'en',
'relationship_to_source',
'relationship_extractor',
{
removedValues: ['S1_sId', 'S0_sId'],
}
);
};
await expect(action()).rejects.toThrow(
'Some of the removed values exist in the suggestion: S1_sId. Cannot remove values that are suggested.'
);
});
it('should allow full acceptance, and update entites of all languages, with the properly translated labels', async () => {
const { acceptedSuggestion, metadataValues, allFiles } =
await prepareAndAcceptRelationshipSuggestion(
['S1_sId', 'S3_sId'],
'en',
'relationship_to_source',
'relationship_extractor'
);
expect(acceptedSuggestion.state).toEqual(matchState(true));
expect(metadataValues).toMatchObject([
[
{ value: 'S1_sId', label: 'S1' },
{ value: 'S3_sId', label: 'S3' },
],
[
{ value: 'S1_sId', label: 'S1_es' },
{ value: 'S3_sId', label: 'S3_es' },
],
]);
expect(allFiles).toEqual(relationshipAcceptanceFixtureBase.files);
});
it('should allow partial acceptance, and update entites of all languages, with the properly translated labels', async () => {
const { acceptedSuggestion, metadataValues, allFiles } =
await prepareAndAcceptRelationshipSuggestion(
['S1_sId', 'S3_sId'],
'en',
'relationship_to_source',
'relationship_extractor',
{
addedValues: ['S3_sId'],
}
);
expect(acceptedSuggestion.state).toEqual(matchState(false));
expect(metadataValues).toMatchObject([
[
{ value: 'S1_sId', label: 'S1' },
{ value: 'S2_sId', label: 'S2' },
{ value: 'S3_sId', label: 'S3' },
],
[
{ value: 'S1_sId', label: 'S1_es' },
{ value: 'S2_sId', label: 'S2_es' },
{ value: 'S3_sId', label: 'S3_es' },
],
]);
expect(allFiles).toEqual(relationshipAcceptanceFixtureBase.files);
});
it('should do nothing on partial acceptance if the id is already in the entity metadata', async () => {
const { acceptedSuggestion, metadataValues, allFiles } =
await prepareAndAcceptRelationshipSuggestion(
['S1_sId', 'S3_sId'],
'en',
'relationship_to_source',
'relationship_extractor',
{
addedValues: ['S1_sId'],
}
);
expect(acceptedSuggestion.state).toEqual(matchState(false));
expect(metadataValues).toMatchObject([
[
{ value: 'S1_sId', label: 'S1' },
{ value: 'S2_sId', label: 'S2' },
],
[
{ value: 'S1_sId', label: 'S1_es' },
{ value: 'S2_sId', label: 'S2_es' },
],
]);
expect(allFiles).toEqual(relationshipAcceptanceFixtureBase.files);
});
it('should allow removal through partial acceptance, and update entities of all languages', async () => {
const { acceptedSuggestion, metadataValues, allFiles } =
await prepareAndAcceptRelationshipSuggestion(
['S1_sId', 'S3_sId'],
'en',
'relationship_to_source',
'relationship_extractor',
{
removedValues: ['S2_sId'],
}
);
expect(acceptedSuggestion.state).toEqual(matchState(false));
expect(metadataValues).toMatchObject([
[{ value: 'S1_sId', label: 'S1' }],
[{ value: 'S1_sId', label: 'S1_es' }],
]);
expect(allFiles).toEqual(relationshipAcceptanceFixtureBase.files);
});
it('should do nothing on removal through partial acceptance if the id is not in the entity metadata', async () => {
const { acceptedSuggestion, metadataValues, allFiles } =
await prepareAndAcceptRelationshipSuggestion(
['S1_sId', 'S2_sId'],
'en',
'relationship_to_source',
'relationship_extractor',
{
removedValues: ['S3_sId'],
}
);
expect(acceptedSuggestion.state).toEqual(matchState(true));
expect(metadataValues).toMatchObject([
[
{ value: 'S1_sId', label: 'S1' },
{ value: 'S2_sId', label: 'S2' },
],
[
{ value: 'S1_sId', label: 'S1_es' },
{ value: 'S2_sId', label: 'S2_es' },
],
]);
expect(allFiles).toEqual(relationshipAcceptanceFixtureBase.files);
});
it('should update inherited values per language', async () => {
const { acceptedSuggestion, metadataValues, allFiles } =
await prepareAndAcceptRelationshipSuggestion(
['S1_sId', 'S3_sId'],
'en',
'relationship_with_inheritance',
'relationship_with_inheritance_extractor'
);
expect(acceptedSuggestion.state).toEqual(matchState(true));
expect(metadataValues).toMatchObject([
[
{
value: 'S1_sId',
label: 'S1',
inheritedType: 'text',
inheritedValue: [
{
value: 'inherited text',
},
],
},
{
value: 'S3_sId',
label: 'S3',
inheritedType: 'text',
inheritedValue: [
{
value: 'inherited text 3',
},
],
},
],
[
{
value: 'S1_sId',
label: 'S1_es',
inheritedType: 'text',
inheritedValue: [
{
value: 'inherited text Spanish',
},
],
},
{
value: 'S3_sId',
label: 'S3_es',
inheritedType: 'text',
inheritedValue: [
{
value: 'inherited text 3 Spanish',
},
],
},
],
]);
expect(allFiles).toEqual(relationshipAcceptanceFixtureBase.files);
});
it('should check if the suggested entities are of the correct template', async () => {
const action = async () => {
await prepareAndAcceptRelationshipSuggestion(
['S1_sId', 'other_source'],
'en',
'relationship_to_source',
'relationship_extractor'
);
};
await expect(action()).rejects.toThrow(
'The following sharedIds do not match the content template in the relationship property: other_source.'
);
});
it('should handle relationship properties with any template as content', async () => {
const { acceptedSuggestion, metadataValues, allFiles } =
await prepareAndAcceptRelationshipSuggestion(
['S2_sId', 'other_source_2'],
'en',
'relationship_to_any',
'relationship_to_any_extractor'
);
expect(acceptedSuggestion.state).toEqual(matchState(true));
expect(metadataValues).toMatchObject([
[
{
value: 'S2_sId',
label: 'S2',
},
{
value: 'other_source_2',
label: 'Other Source 2',
},
],
[
{
value: 'S2_sId',
label: 'S2_es',
},
{
value: 'other_source_2',
label: 'Other Source 2 Spanish',
},
],
]);
expect(allFiles).toEqual(relationshipAcceptanceFixtureBase.files);
});
it('should remove or create connections as necessary', async () => {
const { acceptedSuggestion, metadataValues, allFiles } =
await prepareAndAcceptRelationshipSuggestion(
['S1_sId', 'S3_sId'],
'en',
'relationship_to_source',
'relationship_extractor'
);
expect(acceptedSuggestion.state).toEqual(matchState(true));
expect(metadataValues).toMatchObject([
[
{ value: 'S1_sId', label: 'S1' },
{ value: 'S3_sId', label: 'S3' },
],
[
{ value: 'S1_sId', label: 'S1_es' },
{ value: 'S3_sId', label: 'S3_es' },
],
]);
expect(allFiles).toEqual(relationshipAcceptanceFixtureBase.files);
const removedConnection = await db.mongodb
?.collection('connections')
.findOne({ entity: 'S2_sId', template: factory.id('related') });
expect(removedConnection).toBeNull();
const newConnection = await db.mongodb
?.collection('connections')
.findOne({ entity: 'S3_sId' });
expect(newConnection).toMatchObject({
entity: 'S3_sId',
hub: expect.any(ObjectId),
template: factory.id('related'),
});
const newHub = newConnection?.hub;
const pairedConnection = await db.mongodb
?.collection('connections')
.findOne({ entity: 'entityWithRelationships_sId', hub: newHub });
expect(pairedConnection).toMatchObject({
entity: 'entityWithRelationships_sId',
hub: newHub,
});
});
});
});
describe('save()', () => {
beforeEach(async () => {
await db.setupFixturesAndContext(fixtures);
});
describe('on suggestion status error', () => {
it('should mark error in state as well', async () => {
await Suggestions.save(newErroringSuggestion);
expect(await findOneSuggestion({ entityId: 'new_erroring_suggestion' })).toMatchObject({
...newErroringSuggestion,
state: {
error: true,
},
});
const original = await findOneSuggestion({});
const changed: IXSuggestionType = { ...original, status: 'failed' };
await Suggestions.save(changed);
expect(await findOneSuggestion({ _id: original._id })).toMatchObject({
...changed,
state: {
error: true,
},
});
});
});
describe('on suggestion status processing', () => {
it('should mark processing in state as well', async () => {
await Suggestions.save(newProcessingSuggestion);
expect(await findOneSuggestion({ entityId: 'new_processing_suggestion' })).toMatchObject({
...newProcessingSuggestion,
state: {
processing: true,
},
});
const original = await findOneSuggestion({});
const changed: IXSuggestionType = { ...original, status: 'processing' };
await Suggestions.save(changed);
expect(await findOneSuggestion({ _id: original._id })).toMatchObject({
...changed,
state: {
processing: true,
},
});
});
});
});
describe('updateStates()', () => {
beforeAll(async () => {
await db.setupFixturesAndContext(fixtures);
});
it.each(stateUpdateCases)('should mark $reason', async ({ state, suggestionQuery }) => {
const original = await findOneSuggestion(suggestionQuery);
const idQuery = { _id: original._id };
await Suggestions.updateStates(idQuery);
const changed = await findOneSuggestion(idQuery);
expect(changed).toMatchObject({
...original,
state,
});
});
});
describe('setObsolete()', () => {
beforeEach(async () => {
await db.setupFixturesAndContext(fixtures);
});
it('should set the queried suggestions to obsolete state', async () => {
const query = { entityId: 'shared1' };
await Suggestions.setObsolete(query);
const obsoletes = await db.mongodb?.collection('ixsuggestions').find(query).toArray();
expect(obsoletes?.every(s => s.state.obsolete)).toBe(true);
expect(obsoletes?.length).toBe(4);
});
});
describe('markSuggestionsWithoutSegmentation()', () => {
beforeEach(async () => {
await db.setupFixturesAndContext(fixtures);
});
it('should mark the suggestions without segmentation to error state', async () => {
const query = { entityId: 'shared1' };
await Suggestions.markSuggestionsWithoutSegmentation(query);
const notSegmented = await db.mongodb?.collection('ixsuggestions').find(query).toArray();
expect(notSegmented?.every(s => s.state.error)).toBe(true);
});
it('should not mark suggestions when segmentations are correct', async () => {
const query = { entityId: 'shared2' };
await Suggestions.markSuggestionsWithoutSegmentation(query);
const segmented = await db.mongodb
?.collection('ixsuggestions')
.find({ _id: suggestionId })
.toArray();
const notSegmented = await db.mongodb
?.collection('ixsuggestions')
.find({ _id: shared2AgeSuggestionId })
.toArray();
expect(segmented?.length).toBe(1);
expect(segmented?.every(s => s.state?.error)).toBe(false);
expect(notSegmented?.length).toBe(1);
expect(notSegmented?.every(s => s.state.error)).toBe(true);
});
});
describe('saveMultiple()', () => {
beforeEach(async () => {
await db.setupFixturesAndContext(fixtures);
});
it('should handle everything at once', async () => {
const all: IXSuggestionType[] = (await db.mongodb
?.collection('ixsuggestions')
.find({})
.toArray()) as IXSuggestionType[];
const originals = await Promise.all(
stateUpdateCases.map(async ({ suggestionQuery }) => findOneSuggestion(suggestionQuery))
);
const newSuggestions = [newErroringSuggestion, newProcessingSuggestion];
const toSave = all.concat(newSuggestions);
await Suggestions.saveMultiple(toSave);
const changedSuggestions = await Promise.all(
stateUpdateCases.map(async ({ suggestionQuery }) => findOneSuggestion(suggestionQuery))
);
for (let i = 0; i < stateUpdateCases.length; i += 1) {
const original = originals[i];
const { state } = stateUpdateCases[i];
const changed = changedSuggestions[i];
expect(changed).toMatchObject({
...original,
state,
});
}
expect(await findOneSuggestion({ entityId: newErroringSuggestion.entityId })).toMatchObject({
...newErroringSuggestion,
state: {
error: true,
},
});
expect(await findOneSuggestion({ entityId: newProcessingSuggestion.entityId })).toMatchObject(
{
...newProcessingSuggestion,
state: {
processing: true,
},
}
);
});
});
});