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,20 @@
import { type gmail_v1 } from 'googleapis';
import { http, HttpResponse } from 'msw';
import { type MswHandler } from 'test/integration/utils/http-mock.util';
export const gmailHistoryHandler = (
addedMessages: gmail_v1.Schema$Message[],
): MswHandler =>
http.get('*/gmail/v1/users/me/history', () =>
HttpResponse.json<gmail_v1.Schema$ListHistoryResponse>({
history: [
{
messagesAdded: addedMessages.map((message) => ({
message: { id: message.id, threadId: message.threadId },
})),
},
],
historyId: '987654322',
}),
);

View File

@@ -0,0 +1,77 @@
import { type gmail_v1 } from 'googleapis';
import { http, HttpResponse } from 'msw';
import { gmailMessageListHandler } from 'test/integration/google/mocks/gmail-message-list-handler.util';
import { type MswHandler } from 'test/integration/utils/http-mock.util';
import { type MockEntityStore } from 'test/integration/utils/mock-entity-store.util';
const buildBatchMultipartResponse = (
messages: gmail_v1.Schema$Message[],
): { body: string; contentType: string } => {
const boundary = 'batch_boundary';
const subResponses = messages
.map((message) =>
[
`--${boundary}`,
'Content-Type: application/http',
'',
'HTTP/1.1 200 OK',
'Content-Type: application/json; charset=UTF-8',
'',
JSON.stringify(message),
].join('\r\n'),
)
.join('\r\n');
return {
body: `${subResponses}\r\n--${boundary}--`,
contentType: `multipart/mixed; boundary=${boundary}`,
};
};
export const gmailMailboxHandlers = (
inbox: gmail_v1.Schema$Message[],
labelStore: MockEntityStore<gmail_v1.Schema$Label>,
): MswHandler[] => [
http.get('*/gmail/v1/users/me/labels', () =>
HttpResponse.json<gmail_v1.Schema$ListLabelsResponse>({
labels: labelStore.list(),
}),
),
gmailMessageListHandler(inbox),
http.get('*/gmail/v1/users/me/history', () =>
HttpResponse.json<gmail_v1.Schema$ListHistoryResponse>({
history: [],
historyId: inbox[0]?.historyId ?? '987654321',
}),
),
http.get('*/gmail/v1/users/me/messages/:messageId', ({ params }) => {
const message = inbox.find(
(candidate) => candidate.id === params.messageId,
);
if (!message) {
return HttpResponse.json(
{ error: { code: 404, message: 'Not Found' } },
{ status: 404 },
);
}
return HttpResponse.json<gmail_v1.Schema$Message>(message);
}),
http.post('*/batch', async ({ request }) => {
const requestedIds = [
...(await request.text()).matchAll(/messages\/([\w-]+)/g),
].map((match) => match[1]);
const requestedMessages = inbox.filter((message) =>
requestedIds.includes(message.id ?? ''),
);
const { body, contentType } =
buildBatchMultipartResponse(requestedMessages);
return new HttpResponse(body, {
headers: { 'Content-Type': contentType },
});
}),
];

View File

@@ -0,0 +1,34 @@
import { type gmail_v1 } from 'googleapis';
import { http, HttpResponse } from 'msw';
import { type MswHandler } from 'test/integration/utils/http-mock.util';
// Only the onboarding import sends a positive `in:` term. The full sync's
// `-label:` / `-category:` exclusions and `label:` inclusions stay unapplied.
const INCLUDED_LABEL_REGEX = /(?:^|\s)in:(\w+)/g;
const matchesSearchFilter = (
message: gmail_v1.Schema$Message,
searchFilter: string,
): boolean =>
[...searchFilter.matchAll(INCLUDED_LABEL_REGEX)].every(([, labelName]) =>
message.labelIds?.includes(labelName.toUpperCase()),
);
export const gmailMessageListHandler = (
messages: gmail_v1.Schema$Message[],
): MswHandler =>
http.get('*/gmail/v1/users/me/messages', ({ request }) => {
const searchFilter = new URL(request.url).searchParams.get('q') ?? '';
const matchingMessages = messages.filter((message) =>
matchesSearchFilter(message, searchFilter),
);
return HttpResponse.json<gmail_v1.Schema$ListMessagesResponse>({
messages: matchingMessages.map((message) => ({
id: message.id,
threadId: message.threadId,
})),
resultSizeEstimate: matchingMessages.length,
});
});

View File

@@ -0,0 +1,7 @@
import { type gmail_v1 } from 'googleapis';
export const getGmailMessageSubject = (
message: gmail_v1.Schema$Message,
): string =>
message.payload?.headers?.find((header) => header.name === 'Subject')
?.value ?? '';

View File

@@ -0,0 +1,39 @@
import { randomUUID } from 'node:crypto';
import { type gmail_v1 } from 'googleapis';
type GmailMessageOverrides = Partial<gmail_v1.Schema$Message> & {
from?: string;
to?: string;
};
export const gmailMessage = ({
from,
to,
...overrides
}: GmailMessageOverrides = {}): gmail_v1.Schema$Message => {
const id = overrides.id ?? `gmail-msg-${randomUUID()}`;
return {
id,
threadId: id,
historyId: '987654321',
internalDate: '1700000000000',
labelIds: ['INBOX'],
payload: {
mimeType: 'text/plain',
headers: [
{ name: 'From', value: from ?? `sender-${id}@example.com` },
{ name: 'To', value: to ?? `recipient-${id}@example.com` },
{ name: 'Subject', value: `Subject ${id}` },
{ name: 'Message-ID', value: `<${id}@example.com>` },
{ name: 'Date', value: 'Wed, 15 Nov 2023 00:00:00 +0000' },
],
body: {
data: Buffer.from(`body ${id}`).toString('base64'),
size: 10,
},
},
...overrides,
};
};

View File

@@ -0,0 +1,22 @@
import { randomUUID } from 'node:crypto';
import { type calendar_v3 } from 'googleapis';
export const googleCalendarEvent = (
overrides: Partial<calendar_v3.Schema$Event> = {},
): calendar_v3.Schema$Event => {
const id = overrides.id ?? `google-calendar-event-${randomUUID()}`;
return {
id,
iCalUID: `${id}@google.com`,
summary: `Calendar event ${id}`,
status: 'confirmed',
start: { dateTime: '2023-11-15T10:00:00Z' },
end: { dateTime: '2023-11-15T11:00:00Z' },
created: '2023-11-01T00:00:00.000Z',
updated: '2023-11-01T00:00:00.000Z',
attendees: [],
...overrides,
};
};

View File

@@ -0,0 +1,22 @@
import { type calendar_v3 } from 'googleapis';
import { http, HttpResponse } from 'msw';
import { GOOGLE_CALENDAR_EVENTS_URL } from 'test/integration/google/mocks/google-calendar-events-url.constant';
import { type MswHandler } from 'test/integration/utils/http-mock.util';
export const googleCalendarEventsHandlers = (
events: calendar_v3.Schema$Event[],
nextSyncToken: string,
): MswHandler[] => [
http.get(GOOGLE_CALENDAR_EVENTS_URL, () =>
HttpResponse.json<calendar_v3.Schema$Events>({
items: events,
nextSyncToken,
}),
),
...events.map((event) =>
http.get(`${GOOGLE_CALENDAR_EVENTS_URL}/${event.id}`, () =>
HttpResponse.json<calendar_v3.Schema$Event>(event),
),
),
];

View File

@@ -0,0 +1,2 @@
export const GOOGLE_CALENDAR_EVENTS_URL =
'https://www.googleapis.com/calendar/v3/calendars/primary/events';

View File

@@ -0,0 +1,37 @@
import { http, HttpResponse } from 'msw';
import { GOOGLE_OAUTH_SCOPES } from 'test/integration/google/mocks/google-oauth-scopes.constant';
import { type MswHandler } from 'test/integration/utils/http-mock.util';
export const googleIdentityHandlers = (
handle: string,
aliases: string[] = [],
): MswHandler[] => [
http.get('https://www.googleapis.com/oauth2/v3/userinfo', () =>
HttpResponse.json({
sub: `google-user-id-${handle}`,
email: handle,
email_verified: true,
name: 'Jane Austen',
given_name: 'Jane',
family_name: 'Austen',
}),
),
http.get('https://www.googleapis.com/oauth2/v3/tokeninfo', () =>
HttpResponse.json({ scope: GOOGLE_OAUTH_SCOPES, email: handle }),
),
http.get('https://gmail.googleapis.com/gmail/v1/users/me/profile', () =>
HttpResponse.json({ emailAddress: handle, messagesTotal: 0 }),
),
http.get('https://people.googleapis.com/v1/people/me', () =>
HttpResponse.json({ names: [{ displayName: 'Jane Austen' }] }),
),
http.get('*/gmail/v1/users/me/settings/sendAs', () =>
HttpResponse.json({
sendAs: [
{ sendAsEmail: handle, isPrimary: true },
...aliases.map((alias) => ({ sendAsEmail: alias, isPrimary: false })),
],
}),
),
];

View File

@@ -0,0 +1,9 @@
export const GOOGLE_OAUTH_SCOPES = [
'email',
'profile',
'https://www.googleapis.com/auth/gmail.readonly',
'https://www.googleapis.com/auth/calendar.events',
'https://www.googleapis.com/auth/profile.emails.read',
'https://www.googleapis.com/auth/gmail.send',
'https://www.googleapis.com/auth/gmail.compose',
].join(' ');

View File

@@ -0,0 +1,22 @@
import { http, HttpResponse } from 'msw';
import { GOOGLE_OAUTH_SCOPES } from 'test/integration/google/mocks/google-oauth-scopes.constant';
import { type MswHandler } from 'test/integration/utils/http-mock.util';
export const GOOGLE_TOKEN_URLS = [
'https://oauth2.googleapis.com/token',
'https://www.googleapis.com/oauth2/v4/token',
];
export const googleTokenHandlers = (): MswHandler[] =>
GOOGLE_TOKEN_URLS.map((url) =>
http.post(url, () =>
HttpResponse.json({
access_token: 'mock-access-token',
refresh_token: 'mock-refresh-token',
expires_in: 3600,
scope: GOOGLE_OAUTH_SCOPES,
token_type: 'Bearer',
}),
),
);

View File

@@ -0,0 +1,231 @@
import { type calendar_v3, type gmail_v1 } from 'googleapis';
import { http, HttpResponse } from 'msw';
import { gmailHistoryHandler } from 'test/integration/google/mocks/gmail-history-handler.util';
import { gmailMailboxHandlers } from 'test/integration/google/mocks/gmail-mailbox-handlers.util';
import { gmailMessageListHandler } from 'test/integration/google/mocks/gmail-message-list-handler.util';
import { googleCalendarEventsHandlers } from 'test/integration/google/mocks/google-calendar-events-handlers.util';
import { GOOGLE_CALENDAR_EVENTS_URL } from 'test/integration/google/mocks/google-calendar-events-url.constant';
import { googleIdentityHandlers } from 'test/integration/google/mocks/google-identity-handlers.util';
import {
GOOGLE_TOKEN_URLS,
googleTokenHandlers,
} from 'test/integration/google/mocks/google-token-handlers.util';
import { setupHttpMock } from 'test/integration/utils/http-mock.util';
import {
createMockEntityStore,
type MockEntityStore,
} from 'test/integration/utils/mock-entity-store.util';
const DEFAULT_LABELS: gmail_v1.Schema$Label[] = [
{ id: 'INBOX', name: 'INBOX', type: 'system' },
{ id: 'SENT', name: 'SENT', type: 'system' },
];
export type GoogleMock = {
labels: MockEntityStore<gmail_v1.Schema$Label>;
sentMessages: Array<{ raw: string; threadId?: string }>;
draftMessages: Array<{ raw: string; threadId?: string }>;
createdCalendarEvents: calendar_v3.Schema$Event[];
actAsAccount: (handle: string) => void;
serveMessageList: (messages: gmail_v1.Schema$Message[]) => void;
serveHistory: (addedMessages: gmail_v1.Schema$Message[]) => void;
serveCalendarEvents: (
events: calendar_v3.Schema$Event[],
options?: { nextSyncToken?: string },
) => void;
rateLimitMessageList: (retryAfterIso: string) => void;
rateLimitCalendarEventList: () => void;
failMessageList: (failure: GoogleApiFailure) => void;
failCalendarEventList: (failure: GoogleApiFailure) => void;
declineTokenRefresh: () => void;
};
export type GoogleApiFailure = {
status: number;
reason: string;
message: string;
};
const googleApiErrorResponse = ({
status,
reason,
message,
}: GoogleApiFailure) =>
HttpResponse.json(
{
error: {
code: status,
message,
errors: [{ reason, message }],
},
},
{ status },
);
export const setupGoogleMock = ({
handle,
aliases = [],
inbox = [],
labels = DEFAULT_LABELS,
}: {
handle: string;
aliases?: string[];
inbox?: gmail_v1.Schema$Message[];
labels?: gmail_v1.Schema$Label[];
}): GoogleMock => {
const labelStore = createMockEntityStore(labels, (label) => label.id ?? '');
const sentMessages: Array<{ raw: string; threadId?: string }> = [];
const draftMessages: Array<{ raw: string; threadId?: string }> = [];
const createdCalendarEvents: calendar_v3.Schema$Event[] = [];
const httpMock = setupHttpMock(
...googleTokenHandlers(),
...googleIdentityHandlers(handle, aliases),
...googleCalendarEventsHandlers([], 'mock-calendar-sync-token'),
...gmailMailboxHandlers(inbox, labelStore),
http.post('*/gmail/v1/users/me/messages/send', async ({ request }) => {
const body = (await request.json()) as { raw: string; threadId?: string };
sentMessages.push({
raw: Buffer.from(body.raw, 'base64url').toString(),
threadId: body.threadId,
});
const id = `gmail-sent-${sentMessages.length}`;
return HttpResponse.json({ id, threadId: body.threadId ?? id });
}),
http.post('*/gmail/v1/users/me/drafts', async ({ request }) => {
const body = (await request.json()) as {
message: { raw: string; threadId?: string };
};
draftMessages.push({
raw: Buffer.from(body.message.raw, 'base64url').toString(),
threadId: body.message.threadId,
});
return HttpResponse.json({ id: `gmail-draft-${draftMessages.length}` });
}),
http.get('*/gmail/v1/users/me/drafts', () =>
HttpResponse.json({
drafts: [
{
id: 'gmail-synced-draft',
message: { id: 'gmail-draft-message' },
},
],
}),
),
http.delete(
'*/gmail/v1/users/me/drafts/:draftId',
() => new HttpResponse(null, { status: 204 }),
),
http.post(GOOGLE_CALENDAR_EVENTS_URL, async ({ request }) => {
const event = (await request.json()) as calendar_v3.Schema$Event;
const id = `google-created-calendar-event-${createdCalendarEvents.length + 1}`;
const createdEvent = {
...event,
id,
iCalUID: `${id}@google.com`,
status: 'confirmed',
created: '2026-08-13T00:00:00.000Z',
updated: '2026-08-13T00:00:00.000Z',
};
createdCalendarEvents.push(createdEvent);
return HttpResponse.json(createdEvent);
}),
);
return {
labels: labelStore,
sentMessages,
draftMessages,
createdCalendarEvents,
actAsAccount: (accountHandle) =>
httpMock.use(...googleIdentityHandlers(accountHandle)),
serveMessageList: (messages) =>
httpMock.use(gmailMessageListHandler(messages)),
serveHistory: (addedMessages) =>
httpMock.use(gmailHistoryHandler(addedMessages)),
serveCalendarEvents: (
events,
{ nextSyncToken = 'mock-calendar-sync-token' } = {},
) => httpMock.use(...googleCalendarEventsHandlers(events, nextSyncToken)),
rateLimitMessageList: (retryAfterIso) =>
httpMock.use(
http.get('*/gmail/v1/users/me/messages', () =>
HttpResponse.json(
{
error: {
code: 429,
message: 'Rate Limit Exceeded',
errors: [
{
reason: 'rateLimitExceeded',
message: `Rate Limit Exceeded. Retry after ${retryAfterIso}`,
},
],
},
},
{ status: 429 },
),
),
),
rateLimitCalendarEventList: () =>
httpMock.use(
http.get(GOOGLE_CALENDAR_EVENTS_URL, () =>
HttpResponse.json(
{
error: {
code: 429,
message: 'Rate Limit Exceeded',
errors: [
{
reason: 'rateLimitExceeded',
message: 'Rate Limit Exceeded',
},
],
},
},
{ status: 429 },
),
),
),
// A channel carrying a sync cursor fetches through the history endpoint
// rather than the message list, so both have to fail for the failure to
// surface regardless of which path the channel takes.
failMessageList: (failure) =>
httpMock.use(
http.get('*/gmail/v1/users/me/messages', () =>
googleApiErrorResponse(failure),
),
http.get('*/gmail/v1/users/me/history', () =>
googleApiErrorResponse(failure),
),
),
failCalendarEventList: (failure) =>
httpMock.use(
http.get(GOOGLE_CALENDAR_EVENTS_URL, () =>
googleApiErrorResponse(failure),
),
),
declineTokenRefresh: () =>
httpMock.use(
...GOOGLE_TOKEN_URLS.map((url) =>
http.post(url, () =>
HttpResponse.json(
{
error: 'invalid_grant',
error_description: 'Token has been revoked',
},
{ status: 400 },
),
),
),
),
};
};