add integration test on global search resolver (#11007)

closes https://github.com/twentyhq/core-team-issues/issues/580
This commit is contained in:
Etienne
2025-03-19 14:22:02 +01:00
committed by GitHub
parent d59c989ecd
commit b8b00e5398
7 changed files with 342 additions and 3 deletions

View File

@ -0,0 +1,7 @@
export const OBJECT_MODEL_COMMON_FIELDS = `
id
name
createdAt
updatedAt
deletedAt
`;

View File

@ -0,0 +1,225 @@
import { randomUUID } from 'crypto';
import { OBJECT_MODEL_COMMON_FIELDS } from 'test/integration/constants/object-model-common-fields';
import { PERSON_GQL_FIELDS } from 'test/integration/constants/person-gql-fields.constants';
import { destroyManyOperationFactory } from 'test/integration/graphql/utils/destroy-many-operation-factory.util';
import { destroyOneOperationFactory } from 'test/integration/graphql/utils/destroy-one-operation-factory.util';
import {
globalSearchFactory,
GlobalSearchFactoryParams,
} from 'test/integration/graphql/utils/global-search-factory.util';
import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util';
import { performCreateManyOperation } from 'test/integration/graphql/utils/perform-create-many-operation.utils';
import {
LISTING_NAME_PLURAL,
LISTING_NAME_SINGULAR,
} from 'test/integration/metadata/suites/object-metadata/constants/test-object-names.constant';
import { createListingCustomObject } from 'test/integration/metadata/suites/object-metadata/utils/create-test-object-metadata.util';
import { deleteOneObjectMetadataItem } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util';
import { findManyObjectsMetadataItems } from 'test/integration/metadata/suites/object-metadata/utils/find-many-objects-metadata-items.util';
import { EachTestingContext } from 'twenty-shared';
import { GlobalSearchRecordDTO } from 'src/engine/core-modules/global-search/dtos/global-search-record-dto';
describe('GlobalSearchResolver', () => {
let listingObjectMetadataId: { objectMetadataId: string };
const [firstPerson, secondPerson, thirdPerson] = [
{ id: randomUUID(), name: { firstName: 'searchInput1' } },
{ id: randomUUID(), name: { firstName: 'searchInput2' } },
{ id: randomUUID(), name: { firstName: 'searchInput3' } },
];
const [apiKey] = [
{
id: randomUUID(),
name: 'record not searchable',
expiresAt: new Date(Date.now()),
},
];
const [firstListing, secondListing] = [
{ id: randomUUID(), name: 'searchInput1' },
{ id: randomUUID(), name: 'searchInput2' },
];
const hasSearchRecord = (
globalSearch: GlobalSearchRecordDTO[],
recordId: string,
) => {
return globalSearch.some(
(item: GlobalSearchRecordDTO) => item.recordId === recordId,
);
};
beforeAll(async () => {
try {
const objectsMetadata = await findManyObjectsMetadataItems();
const listingObjectMetadata = objectsMetadata.find(
(object) => object.nameSingular === LISTING_NAME_SINGULAR,
);
if (listingObjectMetadata) {
listingObjectMetadataId = {
objectMetadataId: listingObjectMetadata.id,
};
} else {
listingObjectMetadataId = await createListingCustomObject();
}
await performCreateManyOperation(
LISTING_NAME_SINGULAR,
LISTING_NAME_PLURAL,
OBJECT_MODEL_COMMON_FIELDS,
[firstListing, secondListing],
);
await performCreateManyOperation('person', 'people', PERSON_GQL_FIELDS, [
firstPerson,
secondPerson,
thirdPerson,
]);
await performCreateManyOperation(
'apiKey',
'apiKeys',
OBJECT_MODEL_COMMON_FIELDS,
[apiKey],
);
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
throw new Error('beforeAll failed');
}
});
afterAll(async () => {
await makeGraphqlAPIRequest(
destroyManyOperationFactory({
objectMetadataSingularName: 'person',
objectMetadataPluralName: 'people',
gqlFields: PERSON_GQL_FIELDS,
filter: {
id: {
in: [firstPerson.id, secondPerson.id, thirdPerson.id],
},
},
}),
).catch((error) => {
// eslint-disable-next-line no-console
console.log(error);
});
await deleteOneObjectMetadataItem(
listingObjectMetadataId.objectMetadataId,
).catch((error) => {
// eslint-disable-next-line no-console
console.log(error);
});
await makeGraphqlAPIRequest(
destroyOneOperationFactory({
objectMetadataSingularName: 'apiKey',
gqlFields: OBJECT_MODEL_COMMON_FIELDS,
recordId: apiKey.id,
}),
).catch((error) => {
// eslint-disable-next-line no-console
console.log(error);
});
});
const testsUseCases: EachTestingContext<{
input: GlobalSearchFactoryParams;
eval: {
definedRecordIds: string[];
undefinedRecordIds: string[];
};
}>[] = [
{
title:
'should return all records for "isSearchable:true" objects when no search input is provided',
context: {
input: {
searchInput: '',
},
eval: {
definedRecordIds: [firstListing.id, secondListing.id],
undefinedRecordIds: [apiKey.id],
},
},
},
{
title: 'should return filtered records when search input is provided',
context: {
input: {
searchInput: 'searchInput1',
},
eval: {
definedRecordIds: [firstPerson.id, firstListing.id],
undefinedRecordIds: [secondPerson.id, secondListing.id],
},
},
},
{
title: 'should return record from included Objects only',
context: {
input: {
searchInput: '',
includedObjectNameSingulars: [LISTING_NAME_SINGULAR],
},
eval: {
definedRecordIds: [firstListing.id, secondListing.id],
undefinedRecordIds: [firstPerson.id, secondPerson.id],
},
},
},
{
title: 'should not return record from excludedObject',
context: {
input: {
searchInput: '',
excludedObjectNameSingulars: ['person'],
},
eval: {
definedRecordIds: [firstListing.id, secondListing.id],
undefinedRecordIds: [firstPerson.id, secondPerson.id],
},
},
},
{
title: 'should return filtered records when filter is provided',
context: {
input: {
searchInput: '',
filter: {
id: { eq: firstListing.id },
},
},
eval: {
definedRecordIds: [firstListing.id],
undefinedRecordIds: [secondListing.id],
},
},
},
];
it.each(testsUseCases)('$title', async ({ context }) => {
const graphqlOperation = globalSearchFactory(context.input);
const response = await makeGraphqlAPIRequest(graphqlOperation);
expect(response.body.data).toBeDefined();
expect(response.body.data.globalSearch).toBeDefined();
const globalSearch = response.body.data.globalSearch;
context.eval.definedRecordIds.length > 0
? expect(globalSearch).not.toHaveLength(0)
: expect(globalSearch).toHaveLength(0);
context.eval.definedRecordIds.forEach((recordId) => {
expect(hasSearchRecord(globalSearch, recordId)).toBeTruthy();
});
context.eval.undefinedRecordIds.forEach((recordId) => {
expect(hasSearchRecord(globalSearch, recordId)).toBeFalsy();
});
});
});

View File

@ -0,0 +1,49 @@
import gql from 'graphql-tag';
import { ObjectRecordFilterInput } from 'src/engine/core-modules/global-search/dtos/object-record-filter-input';
export type GlobalSearchFactoryParams = {
searchInput: string;
excludedObjectNameSingulars?: string[];
includedObjectNameSingulars?: string[];
filter?: ObjectRecordFilterInput;
};
export const globalSearchFactory = ({
searchInput,
excludedObjectNameSingulars,
includedObjectNameSingulars,
filter,
}: GlobalSearchFactoryParams) => ({
query: gql`
query GlobalSearch(
$searchInput: String!
$limit: Int!
$excludedObjectNameSingulars: [String!]
$includedObjectNameSingulars: [String!]
$filter: ObjectRecordFilterInput
) {
globalSearch(
searchInput: $searchInput
limit: $limit
excludedObjectNameSingulars: $excludedObjectNameSingulars
includedObjectNameSingulars: $includedObjectNameSingulars
filter: $filter
) {
recordId
objectSingularName
label
imageUrl
tsRankCD
tsRank
}
}
`,
variables: {
searchInput,
limit: 30,
excludedObjectNameSingulars,
includedObjectNameSingulars,
filter,
},
});

View File

@ -0,0 +1,26 @@
import { randomUUID } from 'crypto';
import { createManyOperationFactory } from 'test/integration/graphql/utils/create-many-operation-factory.util';
import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util';
import { capitalize } from 'twenty-shared';
export const performCreateManyOperation = async (
objectMetadataSingularName: string,
objectMetadataPluralName: string,
gqlFields: string,
data: object[],
) => {
const response = await makeGraphqlAPIRequest(
createManyOperationFactory({
objectMetadataSingularName,
objectMetadataPluralName,
gqlFields,
data: data.map((item) => ({
id: randomUUID(),
...item,
})),
}),
);
return response.body.data[`create${capitalize(objectMetadataPluralName)}`];
};

View File

@ -0,0 +1,2 @@
export const LISTING_NAME_SINGULAR = 'listing';
export const LISTING_NAME_PLURAL = 'listings';

View File

@ -1,10 +1,12 @@
import {
LISTING_NAME_PLURAL,
LISTING_NAME_SINGULAR,
} from 'test/integration/metadata/suites/object-metadata/constants/test-object-names.constant';
import { createOneObjectMetadataFactory } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata-factory.util';
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
const LISTING_NAME_SINGULAR = 'listing';
const LISTING_OBJECT = {
namePlural: 'listings',
namePlural: LISTING_NAME_PLURAL,
nameSingular: LISTING_NAME_SINGULAR,
labelPlural: 'Listings',
labelSingular: 'Listing',

View File

@ -0,0 +1,28 @@
import gql from 'graphql-tag';
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
export const findManyObjectsMetadataItems = async () => {
const query = {
query: gql`
query ObjectMetadataItems {
objects(paging: { first: 1000 }) {
edges {
node {
id
nameSingular
namePlural
}
}
}
}
`,
};
const response = await makeMetadataAPIRequest(query);
return response.body.data.objects.edges.map((edge) => edge.node) as {
id: string;
nameSingular: string;
namePlural: string;
}[];
};