snapshot of https://github.com/twentyhq/twenty.git (main, 2026-09-17; blobs > 131072B omitted)
Some checks failed
App Prod-Parity E2E Dispatch / dispatch (push) Failing after 10s
CD Storybook UI / build (push) Failing after 3m25s
CD Storybook UI / dispatch (push) Has been skipped
CD deploy main / website-changed-files (push) Successful in 18m8s
CI Agent Skills / changed-files-check (push) Successful in 18m9s
CI Create App E2E minimal / changed-files-check (push) Successful in 24s
CI Create App / changed-files-check (push) Successful in 26s
CI Docs / changed-files-check (push) Successful in 26s
CI Emails / changed-files-check (push) Successful in 28s
CI Example App Hello World / changed-files-check (push) Successful in 23s
CI Front / changed-files-check (push) Has been skipped
CI Example App Postcard / changed-files-check (push) Successful in 27s
CI UI / changed-files-check (push) Has been skipped
CI Utils Test / changed-files-check (push) Successful in 45s
CI Twenty Apps / discover (push) Successful in 1m3s
CD deploy main / deploy-main (push) Failing after 3s
CI Agent Skills / agent-skills-validate (push) Has been skipped
CI Create App / create-app-test (lint) (push) Has been skipped
CI Emails / emails-test (push) Has been skipped
CI Front / front-task (lingui:compile) (push) Has been skipped
CI Front / front-task (lint) (push) Has been skipped
CI Example App Postcard / example-app-postcard (push) Has been skipped
CI UI / ui-task (lint) (push) Has been skipped
CI UI / ui-task (pack-size) (push) Has been skipped
CI UI / ui-task (size) (push) Has been skipped
CI UI / ui-task (test) (push) Has been skipped
CI UI / ui-task (typecheck) (push) Has been skipped
CI Utils Test / utils-test (push) Has been skipped
CI Twenty Apps / ${{ matrix.app.name }} (push) Has been skipped
CI Emails / ci-emails-status-check (push) Successful in 2s
CI Example App Hello World / ci-example-app-hello-world-status-check (push) Successful in 2s
CI Example App Postcard / ci-example-app-postcard-status-check (push) Successful in 3s
CI UI / ui-sb-test (push) Has been skipped
CI Utils Test / ci-utils-test-status-check (push) Successful in 2s
CI Twenty Apps / ci-twenty-apps-status-check (push) Successful in 2s
CI UI / ci-ui-status-check (push) Successful in 2s
CI Desktop / desktop (push) Failing after 1m8s
Push docs to Crowdin / Push documentation to Crowdin (push) Failing after 55s
Push translations to Crowdin / Extract and upload translations (push) Failing after 5s
Push website translations to Crowdin / Extract and upload website translations (push) Failing after 5s
CI Create App / create-app-test (test) (push) Has been skipped
CI Create App / create-app-test (typecheck) (push) Has been skipped
CI Docs / docs-lint (push) Has been skipped
CI Example App Hello World / example-app-hello-world (push) Has been skipped
CI Front / front-task (lingui:extract) (push) Has been skipped
CI Front / front-task (test) (push) Has been skipped
CI Front / front-task (typecheck) (push) Has been skipped
CI UI / ui-sb-build (push) Failing after 1m32s
CI Twenty Apps / ${{ matrix.app.name }} (${{ matrix.server-source }}) (push) Has been skipped
CI Agent Skills / agent-skills-publish (push) Has been skipped
CI Create App / ci-create-app-status-check (push) Successful in 2s
CI Agent Skills / agent-skills-remote-source (push) Has been skipped
CI Create App E2E minimal / create-app-e2e-minimal (push) Has been cancelled
CI E2E Main / e2e-test (push) Has been cancelled
CI Front / front-sb-build (push) Has been cancelled
CI Front / front-build (push) Has been cancelled
CI Create App E2E minimal / ci-create-app-e2e-minimal-status-check (push) Has been cancelled
CI E2E Main / ci-e2e-main-status-check (push) Has been cancelled
CI E2E Main / notify-main-ci-failure (push) Has been cancelled
CI E2E Main / qa-scout-comment (push) Has been cancelled
CI Front / front-sb-test (push) Has been cancelled
CI Front / front-sb-screenshots (push) Has been cancelled
CI Front / ci-front-status-check (push) Has been cancelled
DPA Sub-Processor Sync / sync-subprocessors (push) Failing after 3s
AI Catalog Sync / sync-catalog (push) Failing after 2s

This commit is contained in:
upstream-mirror
2026-09-17 23:28:13 +05:00
commit e7da97194a
32688 changed files with 4208681 additions and 0 deletions

View File

@@ -0,0 +1,45 @@
import { type Manifest } from 'twenty-shared/application';
export const buildBaseManifest = ({
appId,
roleId,
overrides,
}: {
appId: string;
roleId: string;
overrides?: Partial<Manifest>;
}): Manifest => ({
application: {
universalIdentifier: appId,
defaultRoleUniversalIdentifier: roleId,
displayName: 'Test Application',
description: 'Test application',
applicationVariables: {},
packageJsonChecksum: null,
yarnLockChecksum: null,
},
roles: [
{
universalIdentifier: roleId,
label: 'Test Role',
description: 'A test role',
},
],
permissionFlags: [],
skills: [],
agents: [],
objects: [],
fields: [],
logicFunctions: [],
frontComponents: [],
publicAssets: [],
views: [],
viewFields: [],
navigationMenuItems: [],
pageLayouts: [],
pageLayoutTabs: [],
pageLayoutWidgets: [],
commandMenuItems: [],
timelineActivityTypes: [],
...overrides,
});

View File

@@ -0,0 +1,55 @@
import {
getFieldUniversalIdentifier,
type ObjectManifest,
} from 'twenty-shared/application';
import { v4 as uuidv4 } from 'uuid';
// System fields (id, createdAt, …) are engine-provisioned and must not be
// manifest-authored: the sync rejects any reserved field name. The label
// identifier still defaults to the engine-derived id field universal
// identifier, which is resolvable without declaring the field.
export const buildDefaultObjectManifest = ({
nameSingular,
namePlural,
labelSingular,
labelPlural,
description,
icon = 'IconTicket',
additionalFields = [],
universalIdentifier,
labelIdentifierFieldMetadataUniversalIdentifier,
applicationUniversalIdentifier,
}: {
nameSingular: string;
namePlural: string;
labelSingular: string;
labelPlural: string;
description?: string;
icon?: string;
additionalFields?: ObjectManifest['fields'];
universalIdentifier?: string;
labelIdentifierFieldMetadataUniversalIdentifier?: string;
applicationUniversalIdentifier: string;
}): ObjectManifest => {
const objectUniversalIdentifier = universalIdentifier ?? uuidv4();
const idFieldUniversalIdentifier = getFieldUniversalIdentifier({
applicationUniversalIdentifier,
objectUniversalIdentifier,
name: 'id',
});
return {
universalIdentifier: objectUniversalIdentifier,
labelIdentifierFieldMetadataUniversalIdentifier:
labelIdentifierFieldMetadataUniversalIdentifier ??
idFieldUniversalIdentifier,
nameSingular,
namePlural,
labelSingular,
labelPlural,
description,
icon,
fields: [...additionalFields],
};
};

View File

@@ -0,0 +1,49 @@
import { createAppTarball } from 'test/integration/metadata/suites/application/utils/create-app-tarball.util';
type RegistryVersionMetadata = {
name: string | null;
dist: { tarball: string };
};
export type StubbedRegistryPackage = {
tarballUrl: string;
tarballBuffer: Buffer;
metadata: RegistryVersionMetadata;
};
// Builds the registry metadata and tarball a stubbed npm registry serves for
// one package version. Overrides let a test forge the mismatches the install
// path must reject: a metadata or package.json name that is not the requested
// package, or a manifest identifier that is not the registration's.
export const buildNpmRegistryPackage = async ({
registryBaseUrl,
packageName,
version,
manifestJson,
packageJsonName = packageName,
metadataName = packageName,
}: {
registryBaseUrl: string;
packageName: string;
version: string;
manifestJson: string;
packageJsonName?: string;
metadataName?: string | null;
}): Promise<StubbedRegistryPackage> => {
const tarballBuffer = await createAppTarball({
'manifest.json': manifestJson,
'package.json': JSON.stringify({ name: packageJsonName, version }),
});
const unscopedName = packageName.split('/').pop() ?? packageName;
const tarballUrl = `${registryBaseUrl}/${packageName}/-/${unscopedName}-${version}.tgz`;
return {
tarballUrl,
tarballBuffer,
metadata: {
name: metadataName,
dist: { tarball: tarballUrl },
},
};
};

View File

@@ -0,0 +1,42 @@
import { uninstallApplication } from 'test/integration/metadata/suites/application/utils/uninstall-application.util';
export const cleanupApplicationAndAppRegistration = async ({
applicationUniversalIdentifier,
}: {
applicationUniversalIdentifier: string;
}) => {
try {
await uninstallApplication({
universalIdentifier: applicationUniversalIdentifier,
expectToFail: false,
});
} catch {
// May fail if the sync never succeeded
}
await globalThis.testDataSource.query(
`UPDATE core."application"
SET "packageJsonFileId" = NULL, "yarnLockFileId" = NULL
WHERE "universalIdentifier" = $1`,
[applicationUniversalIdentifier],
);
await globalThis.testDataSource.query(
`DELETE FROM core."file" WHERE "applicationId" IN (
SELECT id FROM core."application" WHERE "universalIdentifier" = $1
)`,
[applicationUniversalIdentifier],
);
await globalThis.testDataSource.query(
`DELETE FROM core."application"
WHERE "universalIdentifier" = $1`,
[applicationUniversalIdentifier],
);
await globalThis.testDataSource.query(
`DELETE FROM core."applicationRegistration"
WHERE "universalIdentifier" = $1`,
[applicationUniversalIdentifier],
);
};

View File

@@ -0,0 +1,32 @@
import gql from 'graphql-tag';
export const completeApplicationFileUploadsQueryFactory = ({
applicationUniversalIdentifier,
fileIds,
}: {
applicationUniversalIdentifier: string;
fileIds: string[];
}) => ({
query: gql`
mutation CompleteApplicationFileUploads(
$applicationUniversalIdentifier: String!
$fileIds: [UUID!]!
) {
completeApplicationFileUploads(
applicationUniversalIdentifier: $applicationUniversalIdentifier
fileIds: $fileIds
) {
files {
id
path
size
}
errors {
fileId
message
}
}
}
`,
variables: { applicationUniversalIdentifier, fileIds },
});

View File

@@ -0,0 +1,49 @@
import { completeApplicationFileUploadsQueryFactory } from 'test/integration/metadata/suites/application/utils/complete-application-file-uploads-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 { 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';
export type CompleteApplicationFileUploadsResult = {
files: { id: string; path: string; size: number }[];
errors: { fileId: string; message: string }[];
};
export const completeApplicationFileUploads = async ({
applicationUniversalIdentifier,
fileIds,
expectToFail = false,
token,
}: {
applicationUniversalIdentifier: string;
fileIds: string[];
expectToFail?: boolean;
token?: string;
}): CommonResponseBody<{
completeApplicationFileUploads: CompleteApplicationFileUploadsResult;
}> => {
const graphqlOperation = completeApplicationFileUploadsQueryFactory({
applicationUniversalIdentifier,
fileIds,
});
const response = await makeMetadataAPIRequest(graphqlOperation, token);
if (expectToFail === true) {
warnIfNoErrorButExpectedToFail({
response,
errorMessage:
'Complete application file uploads should have failed but did not',
});
}
if (expectToFail === false) {
warnIfErrorButNotExpectedToFail({
response,
errorMessage:
'Complete application file uploads has failed but should not',
});
}
return { data: response.body.data, errors: response.body.errors };
};

View File

@@ -0,0 +1,42 @@
import crypto from 'crypto';
import { promises as fs } from 'fs';
import { tmpdir } from 'os';
import { join } from 'path';
import * as tar from 'tar';
export const createAppTarball = async (
files: Record<string, string>,
): Promise<Buffer> => {
const tempId = crypto.randomUUID();
const sourceDir = join(tmpdir(), `test-app-tarball-src-${tempId}`);
const tarballPath = join(tmpdir(), `test-app-tarball-${tempId}.tar.gz`);
await fs.mkdir(sourceDir, { recursive: true });
for (const [name, content] of Object.entries(files)) {
const filePath = join(sourceDir, name);
const dir = filePath.substring(0, filePath.lastIndexOf('/'));
if (dir !== sourceDir) {
await fs.mkdir(dir, { recursive: true });
}
await fs.writeFile(filePath, content);
}
await tar.create(
{
file: tarballPath,
gzip: true,
cwd: sourceDir,
},
Object.keys(files),
);
const buffer = await fs.readFile(tarballPath);
await fs.rm(sourceDir, { recursive: true, force: true });
await fs.rm(tarballPath, { force: true });
return buffer;
};

View File

@@ -0,0 +1,36 @@
import gql from 'graphql-tag';
export const createApplicationFileUploadsQueryFactory = ({
applicationUniversalIdentifier,
files,
}: {
applicationUniversalIdentifier: string;
files: { fileFolder: string; filePath: string; size: number }[];
}) => ({
query: gql`
mutation CreateApplicationFileUploads(
$applicationUniversalIdentifier: String!
$files: [ApplicationFileUploadRequestInput!]!
) {
createApplicationFileUploads(
applicationUniversalIdentifier: $applicationUniversalIdentifier
files: $files
) {
targets {
fileId
fileFolder
filePath
uploadUrl
contentType
expiresAt
}
errors {
fileFolder
filePath
message
}
}
}
`,
variables: { applicationUniversalIdentifier, files },
});

View File

@@ -0,0 +1,63 @@
import { createApplicationFileUploadsQueryFactory } from 'test/integration/metadata/suites/application/utils/create-application-file-uploads-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 { 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';
export type ApplicationFileUploadTarget = {
fileId: string;
fileFolder: string;
filePath: string;
uploadUrl: string;
contentType: string;
expiresAt: string;
};
export type ApplicationFileUploadError = {
fileFolder: string;
filePath: string;
message: string;
};
export type CreateApplicationFileUploadsResult = {
targets: ApplicationFileUploadTarget[];
errors: ApplicationFileUploadError[];
};
export const createApplicationFileUploads = async ({
applicationUniversalIdentifier,
files,
expectToFail = false,
token,
}: {
applicationUniversalIdentifier: string;
files: { fileFolder: string; filePath: string; size: number }[];
expectToFail?: boolean;
token?: string;
}): CommonResponseBody<{
createApplicationFileUploads: CreateApplicationFileUploadsResult;
}> => {
const graphqlOperation = createApplicationFileUploadsQueryFactory({
applicationUniversalIdentifier,
files,
});
const response = await makeMetadataAPIRequest(graphqlOperation, token);
if (expectToFail === true) {
warnIfNoErrorButExpectedToFail({
response,
errorMessage:
'Create application file uploads should have failed but did not',
});
}
if (expectToFail === false) {
warnIfErrorButNotExpectedToFail({
response,
errorMessage: 'Create application file uploads has failed but should not',
});
}
return { data: response.body.data, errors: response.body.errors };
};

View File

@@ -0,0 +1,34 @@
import gql from 'graphql-tag';
export const exportApplicationQueryFactory = ({
universalIdentifier,
}: {
universalIdentifier: string;
}) => ({
query: gql`
query ExportApplication($universalIdentifier: UUID!) {
exportApplication(universalIdentifier: $universalIdentifier) {
application {
universalIdentifier
displayName
sourceType
}
manifest
coverage {
metadataName
universalIdentifier
status
reason
}
files {
folder
path
content
}
}
}
`,
variables: {
universalIdentifier,
},
});

View File

@@ -0,0 +1,51 @@
import { exportApplicationQueryFactory } from 'test/integration/metadata/suites/application/utils/export-application-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 { 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 Manifest } from 'twenty-shared/application';
import { type ApplicationExportCoverageEntryDTO } from 'src/engine/core-modules/application/application-development/dtos/application-export.dto';
export const exportApplication = async ({
universalIdentifier,
expectToFail = false,
token,
}: {
universalIdentifier: string;
expectToFail?: boolean;
token?: string;
}): CommonResponseBody<{
exportApplication: {
application: {
universalIdentifier: string;
displayName: string;
sourceType: string;
};
manifest: Manifest;
coverage: ApplicationExportCoverageEntryDTO[];
files: { folder: string; path: string; content: string }[];
};
}> => {
const graphqlOperation = exportApplicationQueryFactory({
universalIdentifier,
});
const response = await makeMetadataAPIRequest(graphqlOperation, token);
if (expectToFail === true) {
warnIfNoErrorButExpectedToFail({
response,
errorMessage: 'Application export should have failed but did not',
});
}
if (expectToFail === false) {
warnIfErrorButNotExpectedToFail({
response,
errorMessage: 'Application export has failed but should not',
});
}
return { data: response.body.data, errors: response.body.errors };
};

View File

@@ -0,0 +1,24 @@
export type ApplicationRegistrationRow = {
id: string;
name: string;
sourcePackage: string | null;
latestAvailableVersion: string | null;
isVetted: boolean;
logoFileId: string | null;
};
export const findApplicationRegistrationByUniversalIdentifier = async ({
universalIdentifier,
}: {
universalIdentifier: string;
}): Promise<ApplicationRegistrationRow | null> => {
const [row] = await globalThis.testDataSource.query(
`SELECT id, name, "sourcePackage", "latestAvailableVersion", "isVetted",
"logoFileId"
FROM core."applicationRegistration"
WHERE "universalIdentifier" = $1`,
[universalIdentifier],
);
return row ?? null;
};

View File

@@ -0,0 +1,25 @@
import gql from 'graphql-tag';
export const generateApplicationTokenQueryFactory = ({
applicationId,
}: {
applicationId: string;
}) => ({
query: gql`
mutation GenerateApplicationToken($applicationId: UUID!) {
generateApplicationToken(applicationId: $applicationId) {
applicationAccessToken {
token
expiresAt
}
applicationRefreshToken {
token
expiresAt
}
}
}
`,
variables: {
applicationId,
},
});

View File

@@ -0,0 +1,41 @@
import { generateApplicationTokenQueryFactory } from 'test/integration/metadata/suites/application/utils/generate-application-token-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 { 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 ApplicationTokenPairDTO } from 'src/engine/core-modules/application/application-oauth/dtos/application-token-pair.dto';
export const generateApplicationToken = async ({
applicationId,
expectToFail = false,
token,
}: {
applicationId: string;
expectToFail?: boolean;
token?: string;
}): CommonResponseBody<{
generateApplicationToken: ApplicationTokenPairDTO;
}> => {
const graphqlOperation = generateApplicationTokenQueryFactory({
applicationId,
});
const response = await makeMetadataAPIRequest(graphqlOperation, token);
if (expectToFail === true) {
warnIfNoErrorButExpectedToFail({
response,
errorMessage: 'Generate application token should have failed but did not',
});
}
if (expectToFail === false) {
warnIfErrorButNotExpectedToFail({
response,
errorMessage: 'Generate application token has failed but should not',
});
}
return { data: response.body.data, errors: response.body.errors };
};

View File

@@ -0,0 +1,53 @@
import crypto from 'crypto';
import { ApplicationRegistrationSourceType } from 'src/engine/core-modules/application/application-registration/enums/application-registration-source-type.enum';
import { SEED_APPLE_WORKSPACE_ID } from 'src/engine/workspace-manager/dev-seeder/core/constants/seeder-workspaces.constant';
export const insertCatalogApplicationRegistration = async ({
universalIdentifier,
name,
sourcePackage = null,
sourceType = ApplicationRegistrationSourceType.NPM,
latestAvailableVersion = '1.0.0',
manifest = null,
category = null,
workspaceId = SEED_APPLE_WORKSPACE_ID,
}: {
universalIdentifier: string;
name: string;
sourcePackage?: string | null;
sourceType?: ApplicationRegistrationSourceType;
latestAvailableVersion?: string | null;
manifest?: Record<string, unknown> | null;
category?: string | null;
workspaceId?: string | null;
}): Promise<string> => {
const id = crypto.randomUUID();
const oAuthClientId = crypto.randomUUID();
await globalThis.testDataSource.query(
`INSERT INTO core."applicationRegistration"
(id, "universalIdentifier", name, "oAuthClientId",
"oAuthRedirectUris", "oAuthScopes", "workspaceId",
"sourceType", "sourcePackage", "latestAvailableVersion",
"manifest", "isListed", "category")
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)`,
[
id,
universalIdentifier,
name,
oAuthClientId,
[],
[],
workspaceId,
sourceType,
sourcePackage,
latestAvailableVersion,
manifest ? JSON.stringify(manifest) : null,
true,
category,
],
);
return id;
};

View File

@@ -0,0 +1,30 @@
import gql from 'graphql-tag';
export type InstallApplicationFactoryInput = {
universalIdentifier: string;
version?: string;
};
export const installApplicationQueryFactory = ({
input,
}: {
input: InstallApplicationFactoryInput;
}) => ({
query: gql`
mutation InstallApplication(
$universalIdentifier: String!
$version: String
) {
installApplication(
universalIdentifier: $universalIdentifier
version: $version
) {
id
}
}
`,
variables: {
universalIdentifier: input.universalIdentifier,
version: input.version,
},
});

View File

@@ -0,0 +1,40 @@
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
import {
type InstallApplicationFactoryInput,
installApplicationQueryFactory,
} from 'test/integration/metadata/suites/application/utils/install-application-query-factory.util';
import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.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';
export const installApplication = async ({
input,
expectToFail = false,
token,
}: {
input: InstallApplicationFactoryInput;
expectToFail?: boolean;
token?: string;
}): CommonResponseBody<{
installApplication: { id: string };
}> => {
const graphqlOperation = installApplicationQueryFactory({ input });
const response = await makeMetadataAPIRequest(graphqlOperation, token);
if (expectToFail === true) {
warnIfNoErrorButExpectedToFail({
response,
errorMessage: 'Install application should have failed but did not',
});
}
if (expectToFail === false) {
warnIfErrorButNotExpectedToFail({
response,
errorMessage: 'Install application has failed but should not',
});
}
return { data: response.body.data, errors: response.body.errors };
};

View File

@@ -0,0 +1,18 @@
import request from 'supertest';
import { type ApplicationFileUploadTarget } from 'test/integration/metadata/suites/application/utils/create-application-file-uploads.util';
export const putApplicationFileUploadTarget = ({
uploadTarget,
body,
}: {
uploadTarget: ApplicationFileUploadTarget;
body: Buffer;
}) => {
const { pathname, search } = new URL(uploadTarget.uploadUrl);
return request(`http://localhost:${APP_PORT}`)
.put(`${pathname}${search}`)
.set('Content-Type', uploadTarget.contentType)
.send(body);
};

View File

@@ -0,0 +1,37 @@
import { existsSync, mkdirSync, rmSync, writeFileSync } from 'fs';
import { dirname, join } from 'path';
import { SEED_APPLE_WORKSPACE_ID } from 'src/engine/workspace-manager/dev-seeder/core/constants/seeder-workspaces.constant';
const STORAGE_LOCAL_PATH = join(process.cwd(), '.local-storage');
export const seedBuiltFrontComponentSharedDependenciesFile = ({
workspaceId = SEED_APPLE_WORKSPACE_ID,
applicationUniversalIdentifier,
builtPath,
content,
}: {
workspaceId?: string;
applicationUniversalIdentifier: string;
builtPath: string;
content: string;
}): { cleanup: () => void } => {
const filePath = join(
STORAGE_LOCAL_PATH,
workspaceId,
applicationUniversalIdentifier,
'built-front-component',
builtPath,
);
mkdirSync(dirname(filePath), { recursive: true });
writeFileSync(filePath, content);
return {
cleanup: () => {
if (existsSync(filePath)) {
rmSync(filePath);
}
},
};
};

View File

@@ -0,0 +1,93 @@
import gql from 'graphql-tag';
import { isDefined } from 'twenty-shared/utils';
import { uploadApplicationFile } from 'test/integration/metadata/suites/application/utils/upload-application-file.util';
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
export const setupApplicationForSync = async ({
applicationUniversalIdentifier,
name,
sourcePath,
token,
}: {
applicationUniversalIdentifier: string;
name: string;
description: string;
sourcePath: string;
token?: string;
}) => {
jest.useRealTimers();
const registrationResponse = await makeMetadataAPIRequest(
{
query: gql`
mutation CreateApplicationRegistration(
$input: CreateApplicationRegistrationInput!
) {
createApplicationRegistration(input: $input) {
applicationRegistration {
id
}
}
}
`,
variables: {
input: { name, universalIdentifier: applicationUniversalIdentifier },
},
},
token,
);
if (isDefined(registrationResponse.body.errors)) {
throw new Error(
`Failed to create application registration: ${JSON.stringify(
registrationResponse.body.errors,
)}`,
);
}
const developmentApplicationResponse = await makeMetadataAPIRequest(
{
query: gql`
mutation CreateDevelopmentApplication(
$universalIdentifier: String!
$name: String!
) {
createDevelopmentApplication(
universalIdentifier: $universalIdentifier
name: $name
) {
id
}
}
`,
variables: { universalIdentifier: applicationUniversalIdentifier, name },
},
token,
);
if (isDefined(developmentApplicationResponse.body.errors)) {
throw new Error(
`Failed to create development application: ${JSON.stringify(
developmentApplicationResponse.body.errors,
)}`,
);
}
const packageJson = JSON.stringify({
name: sourcePath,
version: '1.0.0',
});
await uploadApplicationFile({
applicationUniversalIdentifier,
fileFolder: 'Dependencies',
filePath: 'package.json',
fileBuffer: Buffer.from(packageJson),
filename: 'package.json',
expectToFail: false,
token,
});
jest.useFakeTimers();
};

View File

@@ -0,0 +1,61 @@
import { type AxiosInstance } from 'axios';
import { type StubbedRegistryPackage } from 'test/integration/metadata/suites/application/utils/build-npm-registry-package.util';
import { getAppProviderByClassName } from 'test/integration/utils/get-app-provider-by-class-name.util';
import { type SecureHttpClientService } from 'src/engine/core-modules/secure-http-client/secure-http-client.service';
import { type TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
export type NpmRegistryStub = {
requestedUrls: string[];
restore: () => void;
};
// Serves a single package from the running server's registry client, so an
// install never reaches npm. Requests to any other host fall through to the
// real client, leaving unrelated outbound traffic untouched.
export const stubNpmRegistry = (
registryPackage: StubbedRegistryPackage,
): NpmRegistryStub => {
const secureHttpClientService =
getAppProviderByClassName<SecureHttpClientService>(
'SecureHttpClientService',
);
const registryBaseUrl = getAppProviderByClassName<TwentyConfigService>(
'TwentyConfigService',
)
.get('APP_REGISTRY_URL')
.replace(/\/$/, '');
const requestedUrls: string[] = [];
const realGetHttpClient = secureHttpClientService.getHttpClient.bind(
secureHttpClientService,
);
const spy = jest
.spyOn(secureHttpClientService, 'getHttpClient')
.mockImplementation((config, context) => {
const client = realGetHttpClient(config, context);
const realGet = client.get.bind(client);
client.get = (async (url: string, requestConfig?: unknown) => {
if (typeof url !== 'string' || !url.startsWith(registryBaseUrl)) {
return realGet(url, requestConfig);
}
requestedUrls.push(url);
if (url === registryPackage.tarballUrl) {
return { data: registryPackage.tarballBuffer };
}
return { data: registryPackage.metadata };
}) as AxiosInstance['get'];
return client;
});
return {
requestedUrls,
restore: () => spy.mockRestore(),
};
};

View File

@@ -0,0 +1,34 @@
import gql from 'graphql-tag';
import { type Manifest } from 'twenty-shared/application';
export const syncApplicationQueryFactory = ({
manifest,
dryRun,
inferDeletionFromMissingEntities,
}: {
manifest: Manifest;
dryRun?: boolean;
inferDeletionFromMissingEntities?: boolean;
}) => ({
query: gql`
mutation SyncApplication(
$manifest: JSON!
$dryRun: Boolean
$inferDeletionFromMissingEntities: Boolean
) {
syncApplication(
manifest: $manifest
dryRun: $dryRun
inferDeletionFromMissingEntities: $inferDeletionFromMissingEntities
) {
applicationUniversalIdentifier
actions
}
}
`,
variables: {
manifest,
dryRun,
inferDeletionFromMissingEntities,
},
});

View File

@@ -0,0 +1,52 @@
import { type Manifest } from 'twenty-shared/application';
import { type SyncAction } from 'twenty-shared/metadata';
import { syncApplicationQueryFactory } from 'test/integration/metadata/suites/application/utils/sync-application-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 { 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';
type WorkspaceMigration = {
applicationUniversalIdentifier: string;
actions: SyncAction[];
};
export const syncApplication = async ({
manifest,
expectToFail = false,
token,
dryRun,
inferDeletionFromMissingEntities,
}: {
manifest: Manifest;
expectToFail?: boolean;
token?: string;
dryRun?: boolean;
inferDeletionFromMissingEntities?: boolean;
}): CommonResponseBody<{
syncApplication: WorkspaceMigration;
}> => {
const graphqlOperation = syncApplicationQueryFactory({
manifest,
dryRun,
inferDeletionFromMissingEntities,
});
const response = await makeMetadataAPIRequest(graphqlOperation, token);
if (expectToFail === true) {
warnIfNoErrorButExpectedToFail({
response,
errorMessage: 'Sync application should have failed but did not',
});
}
if (expectToFail === false) {
warnIfErrorButNotExpectedToFail({
response,
errorMessage: 'Sync application has failed but should not',
});
}
return { data: response.body.data, errors: response.body.errors };
};

View File

@@ -0,0 +1,44 @@
import { getAppProviderByClassName } from 'test/integration/utils/get-app-provider-by-class-name.util';
import { type Manifest } from 'twenty-shared/application';
import { type MarketplaceCatalogSyncService } from 'src/engine/core-modules/application/application-marketplace/marketplace-catalog-sync.service';
import { type MarketplaceService } from 'src/engine/core-modules/application/application-marketplace/marketplace.service';
// Runs the catalog sync against a registry that serves a single package, so
// the test never reaches npm.
export const syncMarketplaceCatalogFromRegistryPackage = async ({
packageName,
version,
manifest,
asset = null,
}: {
packageName: string;
version: string;
manifest: Manifest;
asset?: Buffer | null;
}): Promise<void> => {
const marketplaceService =
getAppProviderByClassName<MarketplaceService>('MarketplaceService');
const fetchAppsFromRegistrySpy = jest
.spyOn(marketplaceService, 'fetchAppsFromRegistry')
.mockResolvedValue([
{ name: packageName, version, description: '', author: 'Unknown' },
]);
const fetchManifestFromRegistryCdnSpy = jest
.spyOn(marketplaceService, 'fetchManifestFromRegistryCdn')
.mockResolvedValue(manifest);
const fetchAssetFromRegistryCdnSpy = jest
.spyOn(marketplaceService, 'fetchAssetFromRegistryCdn')
.mockResolvedValue(asset);
try {
await getAppProviderByClassName<MarketplaceCatalogSyncService>(
'MarketplaceCatalogSyncService',
).syncCatalog();
} finally {
fetchAppsFromRegistrySpy.mockRestore();
fetchManifestFromRegistryCdnSpy.mockRestore();
fetchAssetFromRegistryCdnSpy.mockRestore();
}
};

View File

@@ -0,0 +1,16 @@
import gql from 'graphql-tag';
export const uninstallApplicationQueryFactory = ({
universalIdentifier,
}: {
universalIdentifier: string;
}) => ({
query: gql`
mutation UninstallApplication($universalIdentifier: String!) {
uninstallApplication(universalIdentifier: $universalIdentifier)
}
`,
variables: {
universalIdentifier,
},
});

View File

@@ -0,0 +1,39 @@
import { uninstallApplicationQueryFactory } from 'test/integration/metadata/suites/application/utils/uninstall-application-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 { 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';
export const uninstallApplication = async ({
universalIdentifier,
expectToFail = false,
token,
}: {
universalIdentifier: string;
expectToFail?: boolean;
token?: string;
}): CommonResponseBody<{
uninstallApplication: boolean;
}> => {
const graphqlOperation = uninstallApplicationQueryFactory({
universalIdentifier,
});
const response = await makeMetadataAPIRequest(graphqlOperation, token);
if (expectToFail === true) {
warnIfNoErrorButExpectedToFail({
response,
errorMessage: 'Uninstall application should have failed but did not',
});
}
if (expectToFail === false) {
warnIfErrorButNotExpectedToFail({
response,
errorMessage: 'Uninstall application has failed but should not',
});
}
return { data: response.body.data, errors: response.body.errors };
};

View File

@@ -0,0 +1,30 @@
import gql from 'graphql-tag';
import { type AdminUpdateApplicationRegistrationPayload } from 'src/engine/core-modules/application/application-registration/dtos/admin-update-application-registration.input';
export type UpdateAdminApplicationRegistrationFactoryInput = {
id: string;
update: AdminUpdateApplicationRegistrationPayload;
};
export const updateAdminApplicationRegistrationQueryFactory = ({
id,
update,
}: UpdateAdminApplicationRegistrationFactoryInput) => ({
query: gql`
mutation UpdateAdminApplicationRegistration(
$input: AdminUpdateApplicationRegistrationInput!
) {
updateAdminApplicationRegistration(input: $input) {
id
name
isListed
isPreInstalled
isVetted
}
}
`,
variables: {
input: { id, update },
},
});

View File

@@ -0,0 +1,55 @@
import { makeAdminPanelAPIRequestWithGuestRole } from 'test/integration/graphql/suites/admin-panel/utils/make-admin-panel-api-request-with-guest-role.util';
import {
type UpdateAdminApplicationRegistrationFactoryInput,
updateAdminApplicationRegistrationQueryFactory,
} from 'test/integration/metadata/suites/application/utils/update-admin-application-registration-query-factory.util';
import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.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 { makeAdminPanelAPIRequest } from 'test/integration/twenty-config/utils/make-admin-panel-api-request.util';
import { type ApplicationRegistrationEntity } from 'src/engine/core-modules/application/application-registration/application-registration.entity';
type UpdateAdminApplicationRegistrationResult = Pick<
ApplicationRegistrationEntity,
'id' | 'name' | 'isListed' | 'isPreInstalled' | 'isVetted'
>;
export const updateAdminApplicationRegistration = async ({
id,
update,
expectToFail = false,
withGuestRole = false,
}: UpdateAdminApplicationRegistrationFactoryInput & {
expectToFail?: boolean;
withGuestRole?: boolean;
}): CommonResponseBody<{
updateAdminApplicationRegistration: UpdateAdminApplicationRegistrationResult;
}> => {
const graphqlOperation = updateAdminApplicationRegistrationQueryFactory({
id,
update,
});
const response = withGuestRole
? await makeAdminPanelAPIRequestWithGuestRole(graphqlOperation)
: await makeAdminPanelAPIRequest(graphqlOperation);
if (expectToFail === true) {
warnIfNoErrorButExpectedToFail({
response,
errorMessage:
'Update admin application registration should have failed but did not',
});
}
if (expectToFail === false) {
warnIfErrorButNotExpectedToFail({
response,
errorMessage:
'Update admin application registration has failed but should not',
});
}
return { data: response.body.data, errors: response.body.errors };
};

View File

@@ -0,0 +1,32 @@
import gql from 'graphql-tag';
import { type UpdateApplicationRegistrationPayload } from 'src/engine/core-modules/application/application-registration/dtos/update-application-registration.input';
export type UpdateApplicationRegistrationFactoryInput = {
id: string;
// Extra keys are allowed on purpose: the failing cases send fields the
// tenant input type does not declare.
update: UpdateApplicationRegistrationPayload & Record<string, unknown>;
};
export const updateApplicationRegistrationQueryFactory = ({
id,
update,
}: UpdateApplicationRegistrationFactoryInput) => ({
query: gql`
mutation UpdateApplicationRegistration(
$input: UpdateApplicationRegistrationInput!
) {
updateApplicationRegistration(input: $input) {
id
name
isListed
isPreInstalled
isVetted
}
}
`,
variables: {
input: { id, update },
},
});

View File

@@ -0,0 +1,51 @@
import {
type UpdateApplicationRegistrationFactoryInput,
updateApplicationRegistrationQueryFactory,
} from 'test/integration/metadata/suites/application/utils/update-application-registration-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 { 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 ApplicationRegistrationEntity } from 'src/engine/core-modules/application/application-registration/application-registration.entity';
type UpdateApplicationRegistrationResult = Pick<
ApplicationRegistrationEntity,
'id' | 'name' | 'isListed' | 'isPreInstalled' | 'isVetted'
>;
export const updateApplicationRegistration = async ({
id,
update,
expectToFail = false,
token,
}: UpdateApplicationRegistrationFactoryInput & {
expectToFail?: boolean;
token?: string;
}): CommonResponseBody<{
updateApplicationRegistration: UpdateApplicationRegistrationResult;
}> => {
const graphqlOperation = updateApplicationRegistrationQueryFactory({
id,
update,
});
const response = await makeMetadataAPIRequest(graphqlOperation, token);
if (expectToFail === true) {
warnIfNoErrorButExpectedToFail({
response,
errorMessage:
'Update application registration should have failed but did not',
});
}
if (expectToFail === false) {
warnIfErrorButNotExpectedToFail({
response,
errorMessage: 'Update application registration has failed but should not',
});
}
return { data: response.body.data, errors: response.body.errors };
};

View File

@@ -0,0 +1,21 @@
import gql from 'graphql-tag';
export const uploadAppTarballQueryFactory = ({
universalIdentifier,
}: {
universalIdentifier?: string;
}) => ({
query: gql`
mutation UploadAppTarball($file: Upload!, $universalIdentifier: String) {
uploadAppTarball(file: $file, universalIdentifier: $universalIdentifier) {
id
universalIdentifier
name
}
}
`,
variables: {
file: null,
universalIdentifier: universalIdentifier ?? null,
},
});

View File

@@ -0,0 +1,56 @@
import { uploadAppTarballQueryFactory } from 'test/integration/metadata/suites/application/utils/upload-app-tarball-query-factory.util';
import { makeMetadataAPIRequestWithFileUpload } from 'test/integration/metadata/suites/utils/make-metadata-api-request-with-file-upload.util';
import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.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';
type UploadAppTarballResult = {
id: string;
universalIdentifier: string;
name: string;
};
export const uploadAppTarball = async ({
tarballBuffer,
universalIdentifier,
expectToFail = false,
token,
}: {
tarballBuffer: Buffer;
universalIdentifier?: string;
expectToFail?: boolean;
token?: string;
}): CommonResponseBody<{
uploadAppTarball: UploadAppTarballResult;
}> => {
const graphqlOperation = uploadAppTarballQueryFactory({
universalIdentifier,
});
const response = await makeMetadataAPIRequestWithFileUpload(
graphqlOperation,
{
field: 'file',
buffer: tarballBuffer,
filename: 'app.tar.gz',
contentType: 'application/gzip',
},
token,
);
if (expectToFail === true) {
warnIfNoErrorButExpectedToFail({
response,
errorMessage: 'Upload app tarball should have failed but did not',
});
}
if (expectToFail === false) {
warnIfErrorButNotExpectedToFail({
response,
errorMessage: 'Upload app tarball has failed but should not',
});
}
return { data: response.body.data, errors: response.body.errors };
};

View File

@@ -0,0 +1,36 @@
import gql from 'graphql-tag';
export const uploadApplicationFileQueryFactory = ({
applicationUniversalIdentifier,
fileFolder,
filePath,
}: {
applicationUniversalIdentifier: string;
fileFolder: string;
filePath: string;
}) => ({
query: gql`
mutation UploadApplicationFile(
$file: Upload!
$applicationUniversalIdentifier: String!
$fileFolder: FileFolder!
$filePath: String!
) {
uploadApplicationFile(
file: $file
applicationUniversalIdentifier: $applicationUniversalIdentifier
fileFolder: $fileFolder
filePath: $filePath
) {
id
path
}
}
`,
variables: {
file: null,
applicationUniversalIdentifier,
fileFolder,
filePath,
},
});

View File

@@ -0,0 +1,65 @@
import { uploadApplicationFileQueryFactory } from 'test/integration/metadata/suites/application/utils/upload-application-file-query-factory.util';
import { makeMetadataAPIRequestWithFileUpload } from 'test/integration/metadata/suites/utils/make-metadata-api-request-with-file-upload.util';
import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.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';
type UploadedFile = {
id: string;
path: string;
};
export const uploadApplicationFile = async ({
applicationUniversalIdentifier,
fileFolder,
filePath,
fileBuffer,
filename,
contentType = 'application/json',
expectToFail = false,
token,
}: {
applicationUniversalIdentifier: string;
fileFolder: string;
filePath: string;
fileBuffer: Buffer;
filename: string;
contentType?: string;
expectToFail?: boolean;
token?: string;
}): CommonResponseBody<{
uploadApplicationFile: UploadedFile;
}> => {
const graphqlOperation = uploadApplicationFileQueryFactory({
applicationUniversalIdentifier,
fileFolder,
filePath,
});
const response = await makeMetadataAPIRequestWithFileUpload(
graphqlOperation,
{
field: 'file',
buffer: fileBuffer,
filename,
contentType,
},
token,
);
if (expectToFail === true) {
warnIfNoErrorButExpectedToFail({
response,
errorMessage: 'Upload application file should have failed but did not',
});
}
if (expectToFail === false) {
warnIfErrorButNotExpectedToFail({
response,
errorMessage: 'Upload application file has failed but should not',
});
}
return { data: response.body.data, errors: response.body.errors };
};

View File

@@ -0,0 +1,11 @@
import { getAppProviderByClassName } from 'test/integration/utils/get-app-provider-by-class-name.util';
import { type ApplicationRegistrationEntity } from 'src/engine/core-modules/application/application-registration/application-registration.entity';
import { type ApplicationRegistrationService } from 'src/engine/core-modules/application/application-registration/application-registration.service';
export const upsertApplicationRegistrationFromCatalog = (
params: Parameters<ApplicationRegistrationService['upsertFromCatalog']>[0],
): Promise<ApplicationRegistrationEntity | null> =>
getAppProviderByClassName<ApplicationRegistrationService>(
'ApplicationRegistrationService',
).upsertFromCatalog(params);