Fix integration test hang on unmocked requests + de-flake object metadata suite (#22758)

This commit is contained in:
neo773
2026-07-10 14:27:25 +05:30
committed by GitHub
parent b327ab09a7
commit 4cce9b1544
6 changed files with 265 additions and 10459 deletions
@@ -0,0 +1,14 @@
import { type CreateObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/create-object.input';
export type FailingObjectMetadataCreationTestCase = {
input: Partial<Omit<CreateObjectInput, 'workspaceId' | 'dataSourceId'>>;
expected:
| {
errorCode: 'BAD_USER_INPUT';
messageContains: string;
}
| {
errorCode: 'METADATA_VALIDATION_FAILED';
objectValidationMessages: string[];
};
};
@@ -1,32 +1,70 @@
import { type EachTestingContext } from 'twenty-shared/testing';
import { type CreateObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/create-object.input';
import { type FailingObjectMetadataCreationTestCase } from 'test/integration/metadata/suites/object-metadata/common/failing-object-metadata-creation-test-case.type';
export const OBJECT_METADATA_LABEL_FAILING_TEST_CASES: EachTestingContext<
Partial<Omit<CreateObjectInput, 'workspaceId' | 'dataSourceId'>>
>[] = [
{
title: 'when labelSingular is empty',
context: { labelSingular: '' },
},
{
title: 'when labelPlural is empty',
context: { labelPlural: '' },
},
{
title: 'when labelSingular exceeds maximum length',
context: { labelSingular: 'A'.repeat(64) },
},
{
title: 'when labelPlural exceeds maximum length',
context: { labelPlural: 'A'.repeat(64) },
},
{
title: 'when labelSingular contains only whitespace',
context: { labelSingular: ' ' },
},
{
title: 'when labelPlural contains only whitespace',
context: { labelPlural: ' ' },
},
];
const LABEL_TOO_LONG_MESSAGE = 'Object label is too long';
const LABEL_TOO_SHORT_MESSAGE = 'Object label is too short';
export const OBJECT_METADATA_LABEL_FAILING_TEST_CASES: EachTestingContext<FailingObjectMetadataCreationTestCase>[] =
[
{
title: 'when labelSingular is empty',
context: {
input: { labelSingular: '' },
expected: {
errorCode: 'BAD_USER_INPUT',
messageContains: 'labelSingular should not be empty',
},
},
},
{
title: 'when labelPlural is empty',
context: {
input: { labelPlural: '' },
expected: {
errorCode: 'BAD_USER_INPUT',
messageContains: 'labelPlural should not be empty',
},
},
},
{
title: 'when labelSingular exceeds maximum length',
context: {
input: { labelSingular: 'A'.repeat(64) },
expected: {
errorCode: 'METADATA_VALIDATION_FAILED',
objectValidationMessages: [LABEL_TOO_LONG_MESSAGE],
},
},
},
{
title: 'when labelPlural exceeds maximum length',
context: {
input: { labelPlural: 'A'.repeat(64) },
expected: {
errorCode: 'METADATA_VALIDATION_FAILED',
objectValidationMessages: [LABEL_TOO_LONG_MESSAGE],
},
},
},
{
title: 'when labelSingular contains only whitespace',
context: {
input: { labelSingular: ' ' },
expected: {
errorCode: 'METADATA_VALIDATION_FAILED',
objectValidationMessages: [LABEL_TOO_SHORT_MESSAGE],
},
},
},
{
title: 'when labelPlural contains only whitespace',
context: {
input: { labelPlural: ' ' },
expected: {
errorCode: 'METADATA_VALIDATION_FAILED',
objectValidationMessages: [LABEL_TOO_SHORT_MESSAGE],
},
},
},
];
@@ -1,66 +1,150 @@
import { type EachTestingContext } from 'twenty-shared/testing';
import { type CreateObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/create-object.input';
import { type FailingObjectMetadataCreationTestCase } from 'test/integration/metadata/suites/object-metadata/common/failing-object-metadata-creation-test-case.type';
export const OBJECT_METADATA_NAMES_FAILING_TEST_CASES: EachTestingContext<
Partial<Omit<CreateObjectInput, 'workspaceId' | 'dataSourceId'>>
>[] = [
{
title: 'when nameSingular has invalid characters',
context: { nameSingular: 'μ' },
},
{
title: 'when namePlural has invalid characters',
context: { namePlural: 'μ' },
},
{
title: 'when nameSingular is a reserved keyword',
context: { nameSingular: 'user' },
},
{
title: 'when namePlural is a reserved keyword',
context: { namePlural: 'users' },
},
{
title: 'when nameSingular is not camelCased',
context: { nameSingular: 'Not_Camel_Case' },
},
{
title: 'when namePlural is not camelCased',
context: { namePlural: 'Not_Camel_Case' },
},
{
title: 'when namePlural is an empty string',
context: { namePlural: '' },
},
{
title: 'when nameSingular is an empty string',
context: { nameSingular: '' },
},
{
title: 'when nameSingular contains only whitespaces',
context: { nameSingular: ' ' },
},
{
title: 'when nameSingular contains only one char and whitespaces',
context: { nameSingular: ' a a ' },
},
{
title: 'when name exceeds maximum length',
context: { nameSingular: 'a'.repeat(64) },
},
{
title: 'when names are identical',
context: {
nameSingular: 'fooBar',
namePlural: 'fooBar',
const INVALID_NAME_MESSAGE =
'Name is not valid: it must start with lowercase letter and contain only alphanumeric letters';
const RESERVED_NAME_MESSAGE =
'This name is reserved. Use a different name or the system will add "Custom" suffix.';
const IDENTICAL_NAMES_MESSAGE =
'The singular and plural names cannot be the same for an object';
export const OBJECT_METADATA_NAMES_FAILING_TEST_CASES: EachTestingContext<FailingObjectMetadataCreationTestCase>[] =
[
{
title: 'when nameSingular has invalid characters',
context: {
input: { nameSingular: 'μ' },
expected: {
errorCode: 'METADATA_VALIDATION_FAILED',
objectValidationMessages: [INVALID_NAME_MESSAGE],
},
},
},
},
{
title: 'when names with whitespaces result to be identical',
context: {
nameSingular: ' fooBar ',
namePlural: 'fooBar',
{
title: 'when namePlural has invalid characters',
context: {
input: { namePlural: 'μ' },
expected: {
errorCode: 'METADATA_VALIDATION_FAILED',
objectValidationMessages: [INVALID_NAME_MESSAGE],
},
},
},
},
];
{
title: 'when nameSingular is a reserved keyword',
context: {
input: { nameSingular: 'user' },
expected: {
errorCode: 'METADATA_VALIDATION_FAILED',
objectValidationMessages: [RESERVED_NAME_MESSAGE],
},
},
},
{
title: 'when namePlural is a reserved keyword',
context: {
input: { namePlural: 'users' },
expected: {
errorCode: 'METADATA_VALIDATION_FAILED',
objectValidationMessages: [RESERVED_NAME_MESSAGE],
},
},
},
{
title: 'when nameSingular is not camelCased',
context: {
input: { nameSingular: 'Not_Camel_Case' },
expected: {
errorCode: 'METADATA_VALIDATION_FAILED',
objectValidationMessages: [INVALID_NAME_MESSAGE],
},
},
},
{
title: 'when namePlural is not camelCased',
context: {
input: { namePlural: 'Not_Camel_Case' },
expected: {
errorCode: 'METADATA_VALIDATION_FAILED',
objectValidationMessages: [INVALID_NAME_MESSAGE],
},
},
},
{
title: 'when namePlural is an empty string',
context: {
input: { namePlural: '' },
expected: {
errorCode: 'BAD_USER_INPUT',
messageContains: 'namePlural should not be empty',
},
},
},
{
title: 'when nameSingular is an empty string',
context: {
input: { nameSingular: '' },
expected: {
errorCode: 'BAD_USER_INPUT',
messageContains: 'nameSingular should not be empty',
},
},
},
{
title: 'when nameSingular contains only whitespaces',
context: {
input: { nameSingular: ' ' },
expected: {
errorCode: 'METADATA_VALIDATION_FAILED',
objectValidationMessages: ['Name is too short', INVALID_NAME_MESSAGE],
},
},
},
{
title: 'when nameSingular contains only one char and whitespaces',
context: {
input: { nameSingular: ' a a ' },
expected: {
errorCode: 'METADATA_VALIDATION_FAILED',
objectValidationMessages: [INVALID_NAME_MESSAGE],
},
},
},
{
title: 'when name exceeds maximum length',
context: {
input: { nameSingular: 'a'.repeat(64) },
expected: {
errorCode: 'METADATA_VALIDATION_FAILED',
objectValidationMessages: ['Name is too long'],
},
},
},
{
title: 'when names are identical',
context: {
input: {
nameSingular: 'fooBar',
namePlural: 'fooBar',
},
expected: {
errorCode: 'METADATA_VALIDATION_FAILED',
objectValidationMessages: [IDENTICAL_NAMES_MESSAGE],
},
},
},
{
title: 'when names with whitespaces result to be identical',
context: {
input: {
nameSingular: ' fooBar ',
namePlural: 'fooBar',
},
expected: {
errorCode: 'METADATA_VALIDATION_FAILED',
objectValidationMessages: [IDENTICAL_NAMES_MESSAGE],
},
},
},
];
@@ -2,7 +2,6 @@ import { OBJECT_METADATA_LABEL_FAILING_TEST_CASES } from 'test/integration/metad
import { OBJECT_METADATA_NAMES_FAILING_TEST_CASES } from 'test/integration/metadata/suites/object-metadata/common/object-metadata-names-failing-tests-cases';
import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util';
import { getMockCreateObjectInput } from 'test/integration/metadata/suites/object-metadata/utils/generate-mock-create-object-metadata-input';
import { extractRecordIdsAndDatesAsExpectAny } from 'test/utils/extract-record-ids-and-dates-as-expect-any';
import { eachTestingContextFilter } from 'twenty-shared/testing';
const allTestsUseCases = [
@@ -13,15 +12,33 @@ const allTestsUseCases = [
describe('Object metadata creation should fail v2', () => {
it.each(eachTestingContextFilter(allTestsUseCases))(
'$title',
async ({ context }) => {
async ({ context: { input, expected } }) => {
const { errors } = await createOneObjectMetadata({
input: getMockCreateObjectInput(context),
input: getMockCreateObjectInput(input),
expectToFail: true,
});
expect(errors.length).toBe(1);
expect(errors[0]).toMatchSnapshot(
extractRecordIdsAndDatesAsExpectAny(errors[0]),
const [error] = errors;
expect(error.extensions.code).toBe(expected.errorCode);
if (expected.errorCode === 'BAD_USER_INPUT') {
expect(error.message).toContain(expected.messageContains);
return;
}
const objectMetadataFailures = error.extensions.errors.objectMetadata;
expect(objectMetadataFailures).toHaveLength(1);
expect(objectMetadataFailures[0].errors).toEqual(
expect.arrayContaining(
expected.objectValidationMessages.map((message) =>
expect.objectContaining({ message }),
),
),
);
},
);
@@ -1,4 +1,4 @@
import { http, passthrough, type RequestHandler } from 'msw';
import { http, HttpResponse, passthrough, type RequestHandler } from 'msw';
import { setupServer } from 'msw/node';
const localhostPassthroughHandlers = [
@@ -6,7 +6,22 @@ const localhostPassthroughHandlers = [
http.all('http://localhost*', () => passthrough()),
];
const server = setupServer(...localhostPassthroughHandlers);
// With onUnhandledRequest: 'error', msw throws an uncaught InternalError inside
// the interceptor and the pending request never settles, which hangs the caller
// forever (jest swallows the exception). Responding 500 instead makes the caller
// fail immediately with an attributable error.
const unmockedRequestCatchAllHandler = http.all('*', ({ request }) => {
const message = `Unmocked external request in integration test: ${request.method} ${request.url}`;
console.error(message);
return HttpResponse.json({ error: { message } }, { status: 500 });
});
const server = setupServer(
...localhostPassthroughHandlers,
unmockedRequestCatchAllHandler,
);
export type MswHandler = RequestHandler;