Fix integration test hang on unmocked requests + de-flake object metadata suite (#22758)
This commit is contained in:
-10362
File diff suppressed because it is too large
Load Diff
+14
@@ -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[];
|
||||
};
|
||||
};
|
||||
+67
-29
@@ -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],
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
+145
-61
@@ -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],
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
+22
-5
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user