Create comprehensive integration tests for syncable entities in Twenty. Use when writing integration tests for metadata entities, covering validator exceptions, input transpilation errors, and CRUD operations. Tests are MANDATORY for all syncable entities.
Purpose: Create comprehensive test suite covering all validation scenarios, input transpilation exceptions, and successful use cases.
When to use: After completing Steps 1-5. Integration tests are REQUIRED for all syncable entities.
Tests must cover:
Test pattern: Two-file pattern (query factory + wrapper) for each operation.
File: test/integration/metadata/suites/my-entity/utils/create-my-entity-query-factory.util.ts
import gql from 'graphql-tag';
import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type';
import { type CreateMyEntityInput } from 'src/engine/metadata-modules/my-entity/dtos/create-my-entity.input';
export type CreateMyEntityFactoryInput = CreateMyEntityInput;
const DEFAULT_MY_ENTITY_GQL_FIELDS = `
id
name
label
description
isCustom
createdAt
updatedAt
`;
export const createMyEntityQueryFactory = ({
input,
gqlFields = DEFAULT_MY_ENTITY_GQL_FIELDS,
}: PerformMetadataQueryParams<CreateMyEntityFactoryInput>) => ({
query: gql`
mutation CreateMyEntity($input: CreateMyEntityInput!) {
createMyEntity(input: $input) {
${gqlFields}
}
}
`,
variables: {
input,
},
});
File: test/integration/metadata/suites/my-entity/utils/create-my-entity.util.ts
import {
type CreateMyEntityFactoryInput,
createMyEntityQueryFactory,
} from 'test/integration/metadata/suites/my-entity/utils/create-my-entity-query-factory.util';
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type';
import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type';
import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util';
import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util';
import { type MyEntityDto } from 'src/engine/metadata-modules/my-entity/dtos/my-entity.dto';
export const createMyEntity = async ({
input,
gqlFields,
expectToFail = false,
token,
}: PerformMetadataQueryParams<CreateMyEntityFactoryInput>): CommonResponseBody<{
createMyEntity: MyEntityDto;
}> => {
const graphqlOperation = createMyEntityQueryFactory({
input,
gqlFields,
});
const response = await makeMetadataAPIRequest(graphqlOperation, token);
if (expectToFail === true) {
warnIfNoErrorButExpectedToFail({
response,
errorMessage: 'My entity creation should have failed but did not',
});
}
if (expectToFail === false) {
warnIfErrorButNotExpectedToFail({
response,
errorMessage: 'My entity creation has failed but should not',
});
}
return { data: response.body.data, errors: response.body.errors };
};
Required utilities (follow same pattern):
update-my-entity-query-factory.util.ts + update-my-entity.util.tsdelete-my-entity-query-factory.util.ts + delete-my-entity.util.tsFile: test/integration/metadata/suites/my-entity/failing-my-entity-creation.integration-spec.ts
import { expectOneNotInternalServerErrorSnapshot } from 'test/integration/graphql/utils/expect-one-not-internal-server-error-snapshot.util';
import { createMyEntity } from 'test/integration/metadata/suites/my-entity/utils/create-my-entity.util';
import { deleteMyEntity } from 'test/integration/metadata/suites/my-entity/utils/delete-my-entity.util';
import {
eachTestingContextFilter,
type EachTestingContext,
} from 'twenty-shared/testing';
import { isDefined } from 'twenty-shared/utils';
import { type CreateMyEntityInput } from 'src/engine/metadata-modules/my-entity/dtos/create-my-entity.input';
type TestContext = {
input: CreateMyEntityInput;
};
type GlobalTestContext = {
existingEntityLabel: string;
existingEntityName: string;
};
const globalTestContext: GlobalTestContext = {
existingEntityLabel: 'Existing Test Entity',
existingEntityName: 'existingTestEntity',
};
type CreateMyEntityTestingContext = EachTestingContext<TestContext>[];
describe('My entity creation should fail', () => {
let existingEntityId: string | undefined;
beforeAll(async () => {
// Setup: Create entity for uniqueness tests
const { data } = await createMyEntity({
expectToFail: false,
input: {
name: globalTestContext.existingEntityName,
label: globalTestContext.existingEntityLabel,
},
});
existingEntityId = data.createMyEntity.id;
});
afterAll(async () => {
// Cleanup
if (isDefined(existingEntityId)) {
await deleteMyEntity({
expectToFail: false,
input: { id: existingEntityId },
});
}
});
const failingMyEntityCreationTestCases: CreateMyEntityTestingContext = [
// Input transpilation validation
{
title: 'when name is missing',
context: {
input: {
label: 'Entity Missing Name',
} as CreateMyEntityInput,
},
},
{
title: 'when label is missing',
context: {
input: {
name: 'entityMissingLabel',
} as CreateMyEntityInput,
},
},
{
title: 'when name is empty string',
context: {
input: {
name: '',
label: 'Empty Name Entity',
},
},
},
// Validator business logic
{
title: 'when name already exists (uniqueness)',
context: {
input: {
name: globalTestContext.existingEntityName,
label: 'Duplicate Name Entity',
},
},
},
{
title: 'when trying to create standard entity',
context: {
input: {
name: 'myEntity',
label: 'Standard Entity',
isCustom: false,
} as CreateMyEntityInput,
},
},
// Foreign key validation
{
title: 'when parentEntityId does not exist',
context: {
input: {
name: 'invalidParentEntity',
label: 'Invalid Parent Entity',
parentEntityId: '00000000-0000-0000-0000-000000000000',
},
},
},
];
it.each(eachTestingContextFilter(failingMyEntityCreationTestCases))(
'$title',
async ({ context }) => {
const { errors } = await createMyEntity({
expectToFail: true,
input: context.input,
});
expectOneNotInternalServerErrorSnapshot({
errors,
});
},
);
});
Test coverage requirements:
File: test/integration/metadata/suites/my-entity/successful-my-entity-creation.integration-spec.ts
import { createMyEntity } from 'test/integration/metadata/suites/my-entity/utils/create-my-entity.util';
import { deleteMyEntity } from 'test/integration/metadata/suites/my-entity/utils/delete-my-entity.util';
import { type CreateMyEntityInput } from 'src/engine/metadata-modules/my-entity/dtos/create-my-entity.input';
describe('My entity creation should succeed', () => {
let createdEntityId: string;
afterEach(async () => {
if (createdEntityId) {
await deleteMyEntity({
expectToFail: false,
input: { id: createdEntityId },
});
}
});
it('should create entity with minimal required input', async () => {
const { data } = await createMyEntity({
expectToFail: false,
input: {
name: 'minimalEntity',
label: 'Minimal Entity',
},
});
createdEntityId = data?.createMyEntity?.id;
expect(data.createMyEntity).toMatchObject({
id: expect.any(String),
name: 'minimalEntity',
label: 'Minimal Entity',
description: null,
isCustom: true,
createdAt: expect.any(String),
updatedAt: expect.any(String),
});
});
it('should create entity with all optional fields', async () => {
const input = {
name: 'fullEntity',
label: 'Full Entity',
description: 'Entity with all fields specified',
} as const satisfies CreateMyEntityInput;
const { data } = await createMyEntity({
expectToFail: false,
input,
});
createdEntityId = data?.createMyEntity?.id;
expect(data.createMyEntity).toMatchObject({
id: expect.any(String),
name: 'fullEntity',
label: 'Full Entity',
description: 'Entity with all fields specified',
isCustom: true,
});
});
it('should sanitize input by trimming whitespace', async () => {
const { data } = await createMyEntity({
expectToFail: false,
input: {
name: ' entityWithSpaces ',
label: ' Entity With Spaces ',
description: ' Description with spaces ',
},
});
createdEntityId = data?.createMyEntity?.id;
expect(data.createMyEntity).toMatchObject({
id: expect.any(String),
name: 'entityWithSpaces',
label: 'Entity With Spaces',
description: 'Description with spaces',
});
});
it('should handle long text content', async () => {
const longDescription = 'A'.repeat(1000);
const { data } = await createMyEntity({
expectToFail: false,
input: {
name: 'longDescEntity',
label: 'Long Description Entity',
description: longDescription,
},
});
createdEntityId = data?.createMyEntity?.id;
expect(data.createMyEntity).toMatchObject({
id: expect.any(String),
description: longDescription,
});
});
});
Test coverage requirements:
Create similar test files for update and delete operations:
Required files:
failing-my-entity-update.integration-spec.tssuccessful-my-entity-update.integration-spec.tsfailing-my-entity-deletion.integration-spec.tssuccessful-my-entity-deletion.integration-spec.tsafterEach(async () => {
if (createdEntityId) {
await deleteMyEntity({
expectToFail: false,
input: { id: createdEntityId },
});
}
});
const input = {
name: 'myEntity',
label: 'My Entity',
} as const satisfies CreateMyEntityInput;
expectOneNotInternalServerErrorSnapshot({
errors,
});
# Run all entity tests
npx jest test/integration/metadata/suites/my-entity --config=packages/twenty-server/jest.config.mjs
# Run specific test file
npx jest test/integration/metadata/suites/my-entity/failing-my-entity-creation.integration-spec.ts --config=packages/twenty-server/jest.config.mjs
# Update snapshots
npx jest test/integration/metadata/suites/my-entity --updateSnapshot --config=packages/twenty-server/jest.config.mjs
create-my-entity-query-factory.util.ts createdcreate-my-entity.util.ts createdupdate-my-entity-query-factory.util.ts createdupdate-my-entity.util.ts createddelete-my-entity-query-factory.util.ts createddelete-my-entity.util.ts createdexpectOneNotInternalServerErrorSnapshot__snapshots__/ directoryYour integration tests are complete when:
✅ All test utilities created (minimum 6 files) ✅ Failing creation tests cover all validators ✅ Failing update tests cover business rules ✅ Failing deletion tests cover protection rules ✅ Successful tests cover all use cases ✅ All snapshots generated and committed ✅ All tests pass consistently ✅ Test coverage meets requirements (>80%)
✅ Step 6 Complete! → Your syncable entity is fully tested and production-ready!
Congratulations! You've successfully created a new syncable entity in Twenty's workspace migration system.
For complete workflow, see @creating-syncable-entity rule.